To answer this question you need to dig into the LoaderManager
code.
While the documentation for LoaderManager itself isn't clear enough (or there wouldn't be this question), the documentation for LoaderManagerImpl, a subclass of the abstract LoaderManager, is much more enlightening.
initLoader
Call to initialize a particular ID with a Loader. If this ID already has a Loader associated with it, it is left unchanged and any previous callbacks replaced with the newly provided ones. If there is not currently a Loader for the ID, a new one is created and started.
This function should generally be used when a component is initializing, to ensure that a Loader it relies on is created. This allows it to re-use an existing Loader's data if there already is one, so that for example when an Activity is re-created after a configuration change it does not need to re-create its loaders.
restartLoader
Call to re-create the Loader associated with a particular ID. If there is currently a Loader associated with this ID, it will be canceled/stopped/destroyed as appropriate. A new Loader with the given arguments will be created and its data delivered to you once available.
[...] After calling this function, any previous Loaders associated with this ID will be considered invalid, and you will receive no further data updates from them.
There are basically two cases:
initLoader
will only replace the callbacks passed as a parameter but won't cancel or stop the loader. For a CursorLoader
that means the cursor stays open and active (if that was the case before the initLoader
call). `restartLoader, on the other hand, will cancel, stop and destroy the loader (and close the underlying data source like a cursor) and create a new loader (which would also create a new cursor and re-run the query if the loader is a CursorLoader).Here's the simplified code for both methods:
initLoader
LoaderInfo info = mLoaders.get(id);
if (info == null) {
// Loader doesn't already exist -> create new one
info = createAndInstallLoader(id, args, LoaderManager.LoaderCallbacks<Object>)callback);
} else {
// Loader exists -> only replace callbacks
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
restartLoader
LoaderInfo info = mLoaders.get(id);
if (info != null) {
LoaderInfo inactive = mInactiveLoaders.get(id);
if (inactive != null) {
// does a lot of stuff to deal with already inactive loaders
} else {
// Keep track of the previous instance of this loader so we can destroy
// it when the new one completes.
info.mLoader.abandon();
mInactiveLoaders.put(id, info);
}
}
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
As we can see in case the loader doesn't exist (info == null) both methods will create a new loader (info = createAndInstallLoader(...)).
In case the loader already exists initLoader
only replaces the callbacks (info.mCallbacks = ...) while restartLoader
inactivates the old loader (it will be destroyed when the new loader completes its work) and then creates a new one.
Thus said it's now clear when to use initLoader
and when to use restartLoader
and why it makes sense to have the two methods.
initLoader
is used to ensure there's an initialized loader. If none exists a new one is created, if one already exists it's re-used. We always use this method UNLESS we need a new loader because the query to run has changed (not the underlying data but the actual query like in SQL statement for a CursorLoader), in which case we will call restartLoader
.
The Activity/Fragment life cycle has nothing to do with the decision to use one or the other method (and there's no need to keep track of the calls using a one-shot flag as Simon suggested)! This decision is made solely based on the "need" for a new loader. If we want to run the same query we use initLoader
, if we want to run a different query we use restartLoader
.
We could always use restartLoader
but that would be inefficient. After a screen rotation or if the user navigates away from the app and returns later to the same Activity we usually want to show the same query result and so the restartLoader
would unnecessarily re-create the loader and dismiss the underlying (potentially expensive) query result.
It's very important to understand the difference between the data that is loaded and the "query" to load that data. Let's assume we use a CursorLoader querying a table for orders. If a new order is added to that table the CursorLoader uses onContentChanged() to inform the UI to update and show the new order (no need to use restartLoader
in this case). If we want to display only open orders we need a new query and we would use restartLoader
to return a new CursorLoader reflecting the new query.
Is there some relation between the two methods?
They share the code to create a new Loader but they do different things when a loader already exists.
Does calling
restartLoader
always callinitLoader
?
No, it never does.
Can I call
restartLoader
without having to callinitLoader
?
Yes.
Is it safe to call
initLoader
twice to refresh the data?
It's safe to call initLoader
twice but no data will be refreshed.
When should I use one of the two and why?
That should (hopefully) be clear after my explanations above.
Configuration changes
A LoaderManager retains its state across configuration changes (including orientation changes) so you would think there's nothing left for us to do. Think again...
First of all, a LoaderManager doesn't retain the callbacks, so if you do nothing you won't receive calls to your callback methods like onLoadFinished()
and the like and that will very likely break your app.
Therefore we HAVE to call at least initLoader
to restore the callback methods (a restartLoader
is, of course, possible too). The documentation states:
If at the point of call the caller is in its started state, and the requested loader already exists and has generated its data, then callback
onLoadFinished(Loader, D)
will be called immediately (inside of this function) [...].
That means if we call initLoader
after an orientation change, we will get an onLoadFinished
call right away because the data is already loaded (assuming that was the case before the change).
While that sounds straight forward it can be tricky (don't we all love Android...).
We have to distinguish between two cases:
android:configChanges
tag in the manifest. These
components won't receive an onCreate call after e.g. a
screen rotation, so keep in mind to call
initLoader/restartLoader
in another callback method (e.g. in
onActivityCreated(Bundle)
). To be able to initialize the Loader(s),
the loader ids need to be stored (e.g. in a List). Because
the component is retained across configuration changes we can
just loop over the existing loader ids and call initLoader(loaderid,
...)
.outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray)
in
onSaveInstanceState and restore the ids in onCreate:
loaderIdsArray =
savedInstanceState.getIntegerArrayList(loaderIdsKey)
before we make
the initLoader call(s).Calling initLoader
when the Loader has already been created (this typically happens after configuration changes, for example) tells the LoaderManager to deliver the Loader's most recent data to onLoadFinished
immediately. If the Loader has not already been created (when the activity/fragment first launches, for example) the call to initLoader
tells the LoaderManager to call onCreateLoader
to create the new Loader.
Calling restartLoader
destroys an already existing Loader (as well as any existing data associated with it) and tells the LoaderManager to call onCreateLoader
to create the new Loader and to initiate a new load.
The documentation is pretty clear about this too:
initLoader
ensures a Loader is initialized and active. If the loader doesn't already exist, one is created and (if the activity/fragment is currently started) starts the loader. Otherwise the last created loader is re-used.
restartLoader
starts a new or restarts an existing Loader in this manager, registers the callbacks to it, and (if the activity/fragment is currently started) starts loading it. If a loader with the same id has previously been started it will automatically be destroyed when the new loader completes its work. The callback will be delivered before the old loader is destroyed.
I recently hit a problem with multiple loader managers and screen orientation changes and would like to say that after a lot of trial-and-error, the following pattern works for me in both Activities and Fragments:
onCreate: call initLoader(s)
set a one-shot flag
onResume: call restartLoader (or later, as applicable) if the one-shot is not set.
unset the one-shot in either case.
(in other words, set some flag so that initLoader is always run once & that restartLoader is run on the 2nd & subsequent passes through onResume)
Also, remember to assign different ids for each of your loaders within an Activity (which can be a bit of an issue with fragments within that activity if you're not careful with your numbering)
I tried using initLoader only .... didn't seem to work effectively.
Tried initLoader on onCreate with null args (docs say this is ok) & restartLoader (with valid args) in onResume ....docs are wrong & initLoader throws a nullpointer exception.
Tried restartLoader only ... works for a while but blows on 5th or 6th screen re-orientation.
Tried initLoader in onResume; again works for a while & then blows. (specifically the "Called doRetain when not started:"... error)
Tried the following : (excerpt from a cover class that has the loader id passed into the constructor)
/**
* start or restart the loader (why bother with 2 separate functions ?) (now I know why)
*
* @param manager
* @param args
* @deprecated use {@link #restart(LoaderManager, Bundle)} in onResume (as appropriate) and {@link #initialise(LoaderManager, Bundle)} in onCreate
*/
@Deprecated
public void start(LoaderManager manager, Bundle args) {
if (manager.getLoader(this.id) == null) {
manager.initLoader(this.id, args, this);
} else {
manager.restartLoader(this.id, args, this);
}
}
(which I found somewhere in Stack-Overflow)
Again, this worked for a while but still threw the occasional glitch.
in the case of Managers that cannot be started until the results come back from another manager or task (ie. cannot be initialised in onCreate), I only use initLoader. (I may not be correct in this but it seems to work. These secondary loaders are not part of the immediate instance-state so using initLoader may actually be correct in this case)
Looking at the diagrams and docs, I would have thought that initLoader should go in onCreate & restartLoader in onRestart for Activities but that leaves Fragments using some different pattern and I've not had time to investigate if this is actually stable. Can anyone else comment on if they have success with this pattern for activities?
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