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;
}
Related
I have a fragment that's used for editing a project. I load all the data with a loader and then let the user edit it. The problem is that when a user enters some data in EditText and then rotates the device the loader reloads the data and overrides all changes made by the user. Of course when I comment out initLoader() the EditText values are retained after rotation.
What are some common patterns stopping reloading of loaders after orientation change? The easiest solution I can come up with is putting some sort of a flag variable into onSaveInstanceState() and adding an if statement in onLoadFinished() to not reload the data, but I'm wondering if there is a better solution. Below is simplified code from my fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_add_edit_project, container, false);
this.projectAddressInput = (EditText) view.findViewById(R.id.fragment_add_edit_project_address);
return view;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
#Override
public Loader onCreateLoader(int id, Bundle args) {
return new CursorLoader(
getActivity(),
Project.buildProjectUri(this.projectId),
PROJECTION,
null,
null,
null
);
}
#Override
public void onLoadFinished(Loader loader, Object data) {
Cursor cursor = (Cursor) data;
if (cursor != null && cursor.moveToFirst()) {
this.projectAddressInput.setText(cursor.getString(cursor.getColumnIndex(Project.COLUMN_ADDRESS)));
}
}
#Override
public void onLoaderReset(Loader loader) {}
Any suggestions much appreciated.
Declare android:configChanges in your AndroidManifest.xml to instruct Activity Manager not to restart your activity on configuration changed (which as a result will reload your CursorLoader):
<activity
...
android:configChanges="orientation|screenSize" />
If your application doesn't need to update resources during a specific configuration change and you have a performance limitation that requires you to avoid the activity restart, then you can declare that your activity handles the configuration change itself, which prevents the system from restarting your activity.
Reference: http://developer.android.com/guide/topics/resources/runtime-changes.html
I'm fetching data in my activity that is needed by several fragments. After the data is returned, I create the fragments. I was doing this via an AsyncTask, but it led to occasional crashes if the data returned after a screen rotation or the app is backgrounded.
I read up and thought the solution to this was instead using an AsyncTaskLoader. Supposedly it won't callback if your activity's gone, so those errors should be solved. But this now crashes every time because "Can not perform this action (add fragment) inside of onLoadFinished".
How am I supposed to handle this? I don't want my fragments to each have to fetch the data, so it seems like the activity is the right place to put the code.
Thanks!
Edit 1
Here's the relevant code. I don't think the problem is with the code per-se, but more of my whole approach. The exception is pretty clear I shouldn't be creating fragments when I am. I'm just not sure how to do this otherwise.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportLoaderManager().initLoader(BREWERY_LOADER, null, this).forceLoad();
}
//================================================================================
// Loader handlers
//================================================================================
#Override
public Loader<Brewery> onCreateLoader(int id, Bundle args) {
int breweryId = getIntent().getIntExtra(EXTRA_BREWERY_ID, -1);
return new BreweryLoader(this, breweryId);
}
#Override
public void onLoadFinished(Loader<Brewery> loader, Brewery data) {
if (data != null) {
onBreweryReceived(data);
} else {
onBreweryError();
}
}
#Override
public void onLoaderReset(Loader<Brewery> loader) {
}
...
protected void onBreweryReceived(Brewery brewery) {
...
createFragments();
}
...
protected void createFragments() {
FragmentManager fm = getSupportFragmentManager();
//beers fragment
mBeersFragment = (BreweryBeersFragment)fm.findFragmentById(R.id.beersFragmentContainer);
if (mBeersFragment == null) {
mBeersFragment = new BreweryBeersFragment();
fm.beginTransaction()
.add(R.id.beersFragmentContainer, mBeersFragment)
.commit();
Bundle beersBundle = new Bundle();
beersBundle.putInt(BreweryBeersFragment.EXTRA_BREWERY_ID, mBrewery.getId());
mBeersFragment.setArguments(beersBundle);
}
}
Edit 2
My new strategy is to use an IntentService with a ResultReceiver. I null out callbacks in onPause so there's no danger of my activity being hit when it shouldn't be. This feels a lot more heavy-handed than necessary, but AsyncTask and AsyncTaskLoader neither seemed to have everything I needed. Creating fragments in those callback methods doesn't seem to bother Android either.
From the MVC (Model -- View -- Controller) viewpoint, both the Activity and its fragments are Controller, while it is Model that should be responsible for loading data. As to the View, it is defined by the layout xml, you can define custom View classes, but usually you don't.
So create a Model class. Model is responsible for what must survive a screen turn. (Likely, it will be a static singleton; note that Android can kill and re-create the process, so the singleton may get set to null.) Note that Activities use Bundles to send data to themselves in the future.
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;
}
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.
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.