LoaderCallbacks.onLoadFinished() never called with FragmentPager? - android

I'm using FragmentPagerAdapter to implement a tabbed interface. My 0th fragment creates a loader at creation time, and tries reconnecting to the loader in onActivityCreated(). Here's the class:
public class My0thFragment extends Fragment {
private boolean ranOnce = false;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public void onActivityCreated() {
if (!ranOnce) {
// if the first time we're being created, do an initial load of data.
getLoaderManager().initLoader(500, null, mMyCallback).forceLoad();
ranOnce = true;
} else {
// reconnect?
getLoaderManager().initLoader(500, null, mMyCallback);
// sanity check.
printAllLoaders(getLoaderManager());
}
}
private LoaderManager.LoaderCallbacks<Foo> mMyCallback = new LoaderManager.LoaderCallbacks<Foo>() {
#Override
public Loader<Foo> onCreateLoader(int arg0, Bundle arg1) {
return new FooLoader(getActivity(), arg1);
}
#Override
public void onLoadFinished(Loader<Foo> arg0, Foo arg1) {
Log.e(tag, "onLoadFinished()!");
}
#Override
public void onLoaderReset(Loader<Foo> arg0) {
Log.e(tag, "onLoaderReset()!");
}
};
}
And here's the scenario:
App starts, the 0th fragment is created in the FragmentPagerAdapter.
The onActivityCreated() method is called, which creates and starts the loader on the first run.
I quickly switch to a different tab, before the loader has completed yet.
I can see through the logs that the loader finishes, but my callback never gets the onLoadFinished() callback. I assume this is because the fragment is in some sort of detached state, depending on how FragmentPagerAdapter works.
Returning back to the 0th tab, I see onActivityCreated() gets called, and the initLoader() method is called again. The callback still doesn't fire.
I can print all the loaders in the loader manager at this point, and see that my loader is still sitting in the loader manager.
So I'm stuck here, I must be doing something wrong since the Loader stuff must have been designed for easy use by us developers with fragment lifecycles in mind. Can anyone point out what I'm doing wrong here?
Thank you

Probably you have two instances of the same fragment, try destroy loader on destroy fragment:
protected void onDestroy() {
  if (getSupportLoaderManager().getLoader(0) != null) {
           getLoaderManager().getLoader(0).abandon();
           getSupportLoaderManager().destroyLoader(0);
  }
   super.onDestroy();
}

Don't know if you solved your problem or not, but you should call forceLoad() on Fragment.onStart() instead of Fragment.onActivityCreated().
The myCallback.onLoadFinished() is never called because your "myCallback" is not actually attached to the loader until your fragment is started.

See the sample code file FragmentTabsPager.java from API Demos or Support4Demos. It provides a ViewPager and a TabHost that control the same set of Fragments. I hacked it to cover exactly the case that you describe. (Did you contact me directly?) The main thing to note is that the sample app does not detach the Fragments in the ViewPager.
Here's the part where I hacked it:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTabHost = (TabHost)findViewById(android.R.id.tabhost);
mTabHost.setup();
mViewPager = (ViewPager)findViewById(R.id.pager);
mTabsAdapter = new TabsAdapter(this, mTabHost, mViewPager);
mTabsAdapter.addTab(mTabHost.newTabSpec("fraga").setIndicator("Fragment A"),
FragmentA.class, null);
mTabsAdapter.addTab(mTabHost.newTabSpec("fragb").setIndicator("Fragment B"),
FragmentB.class, null);
if (savedInstanceState != null) {
mTabHost.setCurrentTabByTag(savedInstanceState.getString("tab"));
}
}
Fragment A is the one that contains the Loader.

Related

Which Fragment lifecycle methods we should commit FragmentTrasaction to avoid famous java.lang.IllegalStateException

I was wondering, what is the Fragment lifecycle methods, I should commit FragmentTransaction to avoid famous
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
According to http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html, it gives great tip, on how to avoid such exception, by commit FragmentTransaction
FragmentActivity
onCreate()
onResumeFragments()
onPostResume()
Fragment
???
However, how about Fragment? What is the suitable Fragment lifecycle we should commit our fragment? For instance, under very rare situation, I will get exception from Google Play Console crash report, while trying to commit Fragment in another Fragment's onCreate.
public class BuyPortfolioFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final FragmentManager fm = this.getFragmentManager();
// Check to see if we have retained the worker fragment.
this.statusBarUpdaterFragment = (StatusBarUpdaterFragment)fm.findFragmentByTag(STATUS_BAR_UPDATER_FRAGMENT);
if (this.statusBarUpdaterFragment == null) {
this.statusBarUpdaterFragment = StatusBarUpdaterFragment.newInstance();
this.statusBarUpdaterFragment.setTargetFragment(this, 0);
// java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
fm.beginTransaction().add(statusBarUpdaterFragment, STATUS_BAR_UPDATER_FRAGMENT).commit();
} else {
statusBarUpdaterFragment.setTargetFragment(this, 0);
}
p/s I know I can avoid such exception by using commitAllowingStateLoss. I want to use it as last resource.
Fragment's lifecycle state not always matches Activity's. Fragment's method getFragmentManager() returns the FragmentManager of it's hosting Activity (unless it's a child Fragment, if so this method returns the child fragment manager of a hosting Fragment). You may never know in which state is Fragment's hosting Activity unless you make tracking code. So it's really possible that the transaction eventually may be committed after Activity onSaveInstanceState() was called.
I suggest using getChildFragmentManager() and deal with child fragments from fragments.
Or if your intention was really to control Activity Fragments, make accessors for controlling it's state, like
// Activity method
public void showSomeFragment() {
if (mFragmentTransactionsAllowed) {
// do transaction
}
}
// And track the boolean
#Override
protected void onCreate(Bundle b) {
super.onCreate(b);
// override on onCreate() in case if Activity object is reused and state was true
mFragmentTransactionsAllowed = true;
}
#Override
protected void onStart() {
super.onStart();
// override here so that if activity goes foreground but not yet destroyed
mFragmentTransactionsAllowed = true;
}
#Override
protected void onResume() {
super.onResume();
mFragmentTransactionsAllowed = true;
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mFragmentTransactionsAllowed = false;
}

Saving State of Fragments in a FragmentActivity with FragmentTabHost

I have a Fragment Activity with a FragmentTabHost. I add the fragments to the tab using the following code:
mTabHost.addTab(mTabHost.newTabSpec(tab1Name).setIndicator(tabIndicator1),
EventSettingsStep1Fragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec(tab2Name).setIndicator(tabIndicator2),
EventSettingsStep2Fragment.class, null);
When I switch to different tabs, I'd like to retain all the values (view state, etc) so that I have the same data when I switch back to the tab.
I overrode the onSaveInstanceState method & in there, I added values that I want retained to the bundle.
I ran through the methods being called and I have the following:
Switching from Tab1 to Tab2: Tab1:onPause then Tab2:onCreateView, Tab2:onResume
Switching from Tab2 to Tab1: Tab2:onPause then Tab1:onCreateView, Tab1:onResume
onSaveInstanceState is not being called.
Here is the code for one of my fragments:
public class EventSettingsStep1Fragment extends Fragment implements View.OnClickListener {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
if (savedInstanceState != null) {
Log.d(TAG, "restoring onSavedInstanceState");
Gson gson = new Gson();
event = gson.fromJson(savedInstanceState.getString("event"), Event.class);
}
if (event != null) {
//set views
}
return v;
}
#Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
#Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
#Override
public void onSaveInstanceState(Bundle outState) {
Log.d(TAG, "onSaveInstanceState");
super.onSaveInstanceState(outState);
Gson gson = new Gson();
outState.putString("event", gson.toJson(event));
}
}
Why is onSaveInstanceState not being called? Is it only triggered through the FragmentActivity?
onSaveInstanceState is not being called because the framework simply reuses the already-existing instance of the fragment. onSaveInstanceState only gets called when the instance is about to be destroyed and then recreated. This happens for example when you rotate the display and force the hosting activity to be recreated.
onSaveInstanceState is also not called when you push a fragment on the backstack of a FragmentManager. You will have to restore the state from the already existing instance, which can be very annoying. See SO questions How can I maintain fragment state when added to the back stack? and Once for all, how to correctly save instance state of Fragments in back stack? for example.
Basically you will have to do what the answers to these questions suggest: continue using the values of your instance variables and do not rely on a saved instance state.

Loader restarts on orientation change

In the Android documentation for Loaders found at http://developer.android.com/guide/components/loaders.html it says one of the properties of loaders is that:
They automatically reconnect to the last loader's cursor when being recreated after a configuration change. Thus, they don't need to re-query their data.
The following code does not seem to mirror that behaviour, a new Loader is created an finishes querying the ContentResolver, then I rotate the screen and the Loader is re-created!
public class ReportFragment extends Fragment implements LoaderCallbacks<Cursor> {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(1, null, this);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_report, container, false);
return v;
}
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
Log.d("TEST", "Creating loader");
return new CursorLoader(getActivity(), ResourcesContract.Reports.CONTENT_URI, null, null, null, null);
}
public void onLoadFinished(Loader<Cursor> arg0, Cursor arg1) {
Log.d("TEST", "Load finished");
}
public void onLoaderReset(Loader<Cursor> arg0) {
}
}
Here is the output from my logcat:
08-17 16:49:54.474: D/TEST(1833): Creating loader
08-17 16:49:55.074: D/TEST(1833): Load finished
*Here I rotate the screen*
08-17 16:50:38.115: D/TEST(1833): Creating loader
08-17 16:50:38.353: D/TEST(1833): Load finished
Any idea what I'm doing wrong here?
EDIT:
I should note that I'm building to Android Google API's version 8, and using the v4 support library.
2nd EDIT:
This is most likely due to a bug in the support library, take a look at this bug submission if you want further information:
http://code.google.com/p/android/issues/detail?id=20791&can=5&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars
Though this is an old question, I've been experiencing the same issue as the OP. Using a loader, I need to have it restarting when navigating to a new Activity, and then back. But at the same time, I don't want the loader to restart when I rotate the phone's screen.
What I found is that it is possible to achieve this in onRestart(), if you restart the loader BEFORE calling its super.
public class MainActivity extends AppCompatActivity implements
LoaderManager.LoaderCallbacks<Cursor> {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
//Initialize the loader.
getSupportLoaderManager().initLoader(0, null, this);
}
#Override
protected void onRestart() {
//Restart the loader before calling the super.
getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
super.onRestart();
}
...
}
In my opinion you misunderstood what the documentation says. The documentations says, that they don't need to re-query their data, and it is not doing so.
Try to log/insert a breakpoint in your ContentProvider#query() method! The query will be called only on Activity startup, and not after orientation change.
But this is not true for the LoaderCallbacks#onCreateLoader() method. It will be called after every orientation change, but this not means re-querying, it just calls the method so you can change the CursorLoader if you want.
So far I found that retaining fragment Fragment.setRetainInstance(true) prevents recreating loader on orientation change using support library. The loader last results are nicely delivered in onLoadFinished(). It works at least when activity manages single fragment and the fragment is added to activity using FragmentTransaction.
Though this is a bit old question I would like to put my views here.
There is no need for storing additional info in onSaveInstanceState
The framework automatically reconnect to the last loader's cursor when being recreated after a configuration change. Thus, they don't need to re-query their data.
This means in the onCreate function you need to call loaderManager only if the savedInstanceState is null
Ex:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState == null) {
getLoaderManager().initLoader(1, null, this);
}
}
You can simply check to see if the loader already exists onCreate. Then you can either init or restart.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getLoaderManager().getLoader(LOADER_ID) == null) {
getLoaderManager().initLoader(LOADER_ID, null, this);
} else {
getLoaderManager().restartLoader(LOADER_ID);
}
}
You normally pass an ID to your loader so you can reference it later via the loader manager.
Hope this helps!
onCreate() gets called during screen orientation change since the activity gets destroyed and recreated.
Unless you're loading a lot of data then it doesn't hurt to do, but you can try the following if you want (I haven't tested it, but in theory I think it would work).
Declare a static boolean at the top global of the class. I think you'll also need a static cursor to reference
private static boolean dataDownloaded = false;
private static Cursor oldCursor;
Then on onLoadFinished set dataDownloaded = true
Override onSaveInstanceState to save the boolean value
#Override
protected void onSaveInstanceState(Bundle outSave) {
outSave.putBoolen("datadownloaded", dataDownloaded);
oldCursor = adapter.swapCursor(null);
}
and onCreate add the following
if (savedInstanceState != null) {
this.dataDownloaded = savedInstanceState.getBoolean("datadownloaded", false);
}
adjust your onCreateLoader
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader cursorLoader;
if (dataDownloaded) {
cursorLoader = new CursorLoader(getActivity(),
null, projection, null, null, null);
cursorLoader.deliverResult(oldCursor);
} else {
CursorLoader cursorLoader = new CursorLoader(getActivity(),
URI_PATH, projection, null, null, null);
}
return cursorLoader;
}

Delaying Loaders

Is it always necessary to initLoader from onCreate in a Fragment? What if critical arguments for the loader are dependent on the results of another loader?
i.e. You have 2 loaders: LoaderA, and LoaderB. LoaderB needs the result from LoaderA to run. Both LoaderA and LoaderB are initialized in onCreate of a fragment, but LoaderB is given no arguments so that it intentionally fails.
Once LoaderA finishes, LoaderB is restarted with new arguments so that it can perform its desired request.
Loader initialization in fragment:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(LOADER_A, new Bundle(), this);
getLoaderManager().initLoader(LOADER_B, null, mLoaderBCallback);
}
Call backs for LOADER_A in fragment:
#Override
public Loader<MyResultObject> onCreateLoader(int id, Bundle args) {
return new LoaderA(getActivity(), args);
}
#Override
public void onLoadFinished(Loader<MyResultObject> loader, final MyResultObject result) {
if (result != null) {
Bundle args = new Bundle();
args.putInt("id", result.getId());
getLoaderManager().restartLoader(LOADER_B, args, mLoaderBCallback);
}
}
Definition of mLoaderBCallback in fragment:
private LoaderBCallback mLoaderBCallback = new LoaderBCallback();
(The implementation of LoaderBCallback is not important, its just the standard LoaderCallbacks interface that creates an instance of LoaderB and handles when the loader is finished.)
LoaderB class (please excuse any potential compiler errors with this class definition, its just an example):
public class LoaderB<List<AnotherResultObject>> extends AsyncTaskLoader<List<AnotherResultObject>> {
private Bundle mArgs;
public LoaderB(Context context, Bundle args) {
super(context);
mArgs = args;
}
#Override
public List<AnotherResultObject> loadInBackground() {
if (mArgs == null) {
// bail out, no arguments.
return null;
}
// do network request with mArgs here
return MyStaticClass.performAwesomeNetworkRequest(mArgs);
}
// boiler plate AsyncTaskLoader stuff here
......
}
Is there a better way? Can we do without the initLoader for LoaderB?
Edit: I am under the impression that loaders ALWAYS have to be initialized in onCreate, so that they can handle configuration changes. This may be true ONLY for loaders in Activities . Do loaders created in Fragments get managed no matter where they are initialized?
You can init a loader anywhere in your code.
In your case you should replace your restartLoader in onLoadFinished with initLoader. Just remove the initLoader from your onActivityCreated for LOADER_B
Also, you should check the ID of the loader in onLoadFinished so you know which loader finished.
edit: you are using a separate listener for the LOADER_B callback so my ID checking point kinda gets defeated there.. but at any rate.. you can combine them into one if you want
#Override
public void onLoadFinished(Loader<MyResultObject> loader, final MyResultObject result) {
switch (loader.getId())
{
case LOADER_A:
if (result != null) {
Bundle args = new Bundle();
args.putInt("id", result.getId());
// i put "this" as the callback listener. you can use your custom one here if you want
getLoaderManager().initLoader(LOADER_B, args, this);
}
break;
case LOADER_B:
//do whatever
break;
}

Loader can not be restarted after orientation changed

I'd like to use a demo to show this:
enter code here
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button button = (Button) findViewById(R.id.button1);
button.setOnClickListener(buttonClickListener);
}
private OnClickListener buttonClickListener = new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
startMyLoader();
}
};
private void startMyLoader() {
getLoaderManager().destroyLoader(0);
getLoaderManager().restartLoader(0, null, myLoaderListener);
}
/**
* The listener for the group metadata loader.
*/
private final LoaderManager.LoaderCallbacks<Cursor> myLoaderListener
= new LoaderCallbacks<Cursor>() {
#Override
public CursorLoader onCreateLoader(int id, Bundle args) {
return new CursorLoader(LoaderDemoActivity.this,
ContactsContract.Contacts.CONTENT_URI,
null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cursor.moveToPosition(-1);
if (cursor.moveToNext()) {
Context context = getApplicationContext();
CharSequence text = "Load finished!";
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
}
};
enter code here
After orientation changed, I clicked the button,
the onCreateLoader can be called,
but onLoadFinished will not be called.
It seems strange.
Thanks for help in advance.
I faced the same problem. Please make a try call this.getSupportLoaderManager() in onCreate.
It solved my problem. Hope it will help you as well
I think I have found the reason.
In Activity onCreate, it will load all the LoaderMangers(of its own or its sub-Fragments)
from NonConfigurationInstances.
if (mLastNonConfigurationInstances != null) {
mAllLoaderManagers = mLastNonConfigurationInstances.loaders;
}
And in Activity onStart, it will try to start its own LoaderManger.
if (!mLoadersStarted) {
mLoadersStarted = true;
if (mLoaderManager != null) {
mLoaderManager.doStart();
} else if (!mCheckedForLoaderManager) {
mLoaderManager = getLoaderManager(-1, mLoadersStarted, false);
}
mCheckedForLoaderManager = true;
}
But after config changed, mLoaderManager == null, so it will not start it.
And here is the problem!
If you try to start loader belong to this loaderManager, it will fail.
void installLoader(LoaderInfo info) {
mLoaders.put(info.mId, info);
if (mStarted) {
// The activity will start all existing loaders in it's onStart(),
// so only start them here if we're past that point of the activitiy's
// life cycle
info.start();
}
}
note the mStarted value which will be set 'true' when LoaderManager started.
And there is two ways to solve this problem.
call getLoaderManger() in onCreate(), it will re-assign the mLoaderManager
and make it ready to be started in the subseuqent onStart().
public LoaderManager getLoaderManager() {
if (mLoaderManager != null) {
return mLoaderManager;
}
mCheckedForLoaderManager = true;
mLoaderManager = getLoaderManager(-1, mLoadersStarted, true);
return mLoaderManager;
}
have the loader located in fragments. Because in Fragments' onStart(),
it will start its own LoaderManager.
if (!mLoadersStarted) {
mLoadersStarted = true;
if (!mCheckedForLoaderManager) {
mCheckedForLoaderManager = true;
mLoaderManager = mActivity.getLoaderManager(mIndex, mLoadersStarted, false);
}
if (mLoaderManager != null) {
mLoaderManager.doStart();
}
}
You don't need to (neither ought to) destroy your Loader in order to reload it. Loader class is intended to be reusable.
Use initLoader instead. eg.:
getLoaderManager().initLoader(0, null, myLoaderListener);
If you want to force reloading allready registered loader:
getLoaderManager().getLoader(0).forceLoad();
If you are not sure if the Loader instance allready exists after configuration change event happened use initLoader instead of getLoader to retrieve your Loader instance on which you can call forceLoad().
getLoaderManager().initLoader(0, null, myLoaderListener).forceLoad();
If you use support library then use forceLoad even after first instantation - there is probably a bug - I remind myself there are some questions about it on this forum - try searching older posts.
Make sure you are not checking savedStateInfo while using fragments before you call your loader in activity onCreate
#Override
public void onCreate(Bundle savedInstanceState) {
// used to not overlap fragments
if (savedInstanceState != null) {
return null;
}
loadFragments();
getSupportLoaderManager().restartLoader(LISTS_LOADER, null, this);
}
If you need to check for savedInstanceState fragments anyway you can check for any class variable that should be created after loader finished loading, as activity gets destroyed when rotating, but raising from previous state when rotating back.
From the android's development site
"They automatically reconnect to the last loader's cursor when being
recreated after a configuration change. Thus, they don't need to
re-query their data."
As far as I understand even when we start the loader explicitly the loader won't start. Since the destroy which we are calling should actually call onLoaderReset() once it is destroyed. But that method is not called once the orientation is changed, but is called before.
Still I may be wrong in this. This is my assumption. Further discussion would be appreciated.

Categories

Resources