I'm trying to figure out if I'm doing something wrong with respect to Loaders. I'm using the support library, and I have a Fragment which in onCreate() calls initLoader() setting itself as the LoaderCallbacks, however on a rotation it is receiving the result twice in onLoadFinished(), once as a result of calling init (and it already having the data), and once as a result of FragmentActivity looping through all Loaders in onStart() and delivering the result since it already has the data.
If I only call init once (on first launch of the Fragment), it doesn't set itself as the callback for the Loader so it doesn't receive a call to onLoadFinished at all. It seems as though onLoadFinished should only be called once since some expensive things may be done in onLoadFinished() (such as clearing list adapters, etc.), so I'm just trying to figure out if this is a bug or if I am just calling init at the wrong time or something else.
Anyone have any insight to this issue?
I had a similar problem and the cause was that I had initLoader and restartLoader in my code. Depending on user's action, my query could change, so I needed to restart my loader.
The solution was to use only restartLoader, even in onResume callback method replace initLoader with restartLoader.
This is a rather old question, but for future readers I have an alternative solution. Basically what I ended up doing was restart the loader if it existed.
public void onActivityCreated(Bundle savedInstanceState) {
...
if(getLoaderManager().getLoader(Constants.LOADER_ID) == null) {
getLoaderManager().initLoader(Constants.LOADER_ID, null, this);
} else {
getLoaderManager().restartLoader(Constants.LOADER_ID, null, this);
}
...
}
This solved my issue with that on screen rotate the loader was triggered twice. One thing too note is that this is only needed for me on Android < 6 that I tested. Android 6 seem to not have this issue at all.
I am experiencing same problem my self, with no good solution.
It seems as bug in Android framework, here is similar thread in which proposed solution is to place initLoader() in onResume() - I have tried it and it works, on onLoadFinished() gets called only once:
Android: LoaderCallbacks.OnLoadFinished called twice
See my post at Android: LoaderCallbacks.OnLoadFinished called twice
I had a similar problem when restarting Fragments in a ViewPager. My solution is to remove the Loader once I'm finished with it (at the end of onLoadFinished) by calling
getLoaderManager().destroyLoader(LOADER_ID);
Hope it helps!
It is looks like framework Loader wrong implementation/bug.
1. look at what I got from Log.v(LOG_TAG, ...) messages from every important method/callback after screen rotation:
...: .initLoader() 100
...: onStartLoading()
...: onLoadFinished()
...: updateUi(); articles size=10
...: loadInBackground()
...: getInputStream() - HttpRequest
...: onLoadFinished()
...: updateUi(); articles size=10
2. As you can see everything after 'updateUi()' is no needed there.
Related
I am an experienced Android developer and it is a shame that I am still ineffectively fighting against fundamental issues like persisting UI changes across configuration changes.
I know, this is an all too well known problem and there exists a lot of literature on it. The recommended solution is to setRetainInstance(true) a UI-less fragment:
https://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
Best practice: AsyncTask during orientation change
For the sake of completeness, let me draw up the scenario. You have an AsyncTask that performs a Network request and is supposed to update the UI in onPostExecute()
I see some issues with the proposed solution:
The proposed solution says that onPostExecute() is guaranteed to not be called during between the calls to onDetach() and the next onAttach() but what if the AsyncTask returns early(after Activity's onPause() and before the call to onDetach()). If onPostExecute() executes during the period, since onSaveInstanceState() will have been called, any update to the Views will not persist across Activity restart.
Second if the AsyncTask returns such that onPostExecute() executes before the call to Activity's onCreate() then the views will not have been initialized for any view updates to be pushed yet.
Is there any effective solution against this problem that addresses memory leaks and state save across config changes?
I have an app, fetchnig data from API and I use AsyncTaskLoader to update adapter and update UI with new data.
Loader works fine, when rotating device (data is kept unchanged).
Problem occurs, when I leave an app - using 'Home button' or turn off the screen and turn it back on. Then, data adapter and view are automatically updated with new, fetched data.
I have found out that when device is rotated - only onLoadFinished() method is executed.
When I turn off/on screen - onStartLoading() and loadInBackground() Loader methods are executed before onLoadFinished()! I cannot understand why there is such a difference between device rotation and turning off/on screen. I believe that Loader should behave the same in these two situations - ONLY onLoadFinished() should be called.
EDIT: I managed to fix issue:
1. create global 'cache variable' in AsyncTaskLoader implementation
2. store fetched data in deliverResult method in global 'cache variable'
3. in onStartLoading() method check for cache variable, and if it isn't empty => deliverResult(cache variable)
More info:
What does AsyncTaskLoader.deliverResult() actually do?
Yes loaders behavior got changed in 27.1
The underlying implementation of Loaders has been rewritten to use Lifecycle. While the API remains unchanged, there are a number of behavior changes
initLoader(), restartLoader(), and destroyLoader() can now only be
called on the main thread.
A Loader's onStartLoading() and onStopLoading() are now called when
the containing FragmentActivity/Fragment is started and stopped,
respectively.
onLoadFinished() will only be called between onStart() and onStop. As
a result, Fragment transactions can now safely be done in
onLoadFinished().
The FragmentController methods related to Loaders are now deprecated.
https://developer.android.com/topic/libraries/support-library/revisions
Check important changes in it
On a button click I want to create a new SQLite database from two XML files. Using LoaderManager and AsyncTaskLoader seems to be appropriate for this.
The Fragment.onCreate() calls:
setRetainInstance(true);
When the button is clicked, my Fragment sets a flag that it is creating the database, changes the layout to show a ProgressBar and cancel button, and calls:
getLoaderManager().initLoader(0, extra, this);
The operation takes 12 seconds on my PC/Emulator. If I rotate the screen, the loader continues to call the Fragment's onLoaderProgressUpdate(int) (an interface callback method) and completes the creation of the database. But it never calls the onLoadFinished() method, where I can reset the layout, enable buttons and the action bar menu that require the database to be there.
I tried doing this in Fragment.onCreateView():
if (mCreatDBInProgress) {
mBTN_CreateDB.setEnabled(false);
mDBLayout.setVisibility(View.GONE);
mProgressBarLayout.setVisibility(View.VISIBLE);
getLoaderManager().restartLoader(0, null, this);
}
But it calls my onCreateLoader() method and starts the whole thing from scratch. This is a problem, since I have not called the method that deletes the database and goes through the process of determining what XML files to use for creating the database. The XML filenames are what I pass to that method in the Bundle arg.
I don't get why the ProgressBar callback method continues to work, but the AsyncTaskLoader can't call the onLoadFinished() method. LogCat shows that it did the deliverResult().
I tried to make the Bundle a member variable, so I could re-use it on the restartLoader() call. It start another asyncTaskLoader and my ProgressBar goes back and forth as both threads try to update it. Eventually, it gets an SQLConstraintException trying to add a row to the database that was already added by the first thread.
Well, suck me sideways! I came up with one more idea, and it worked!
Instead of calling restartoader() after the rotation, just call:
getLoaderManager().initLoader(0, mExtra, this);
For (whatever) reason, that causes the AsynTaskLoader to just continue with the first thread (instead of starting another) and relink to my Fragment for the onLoadFinished() call.
So:
initLoader() - reconnects the loader to your Fragment IF you call it with the same arguments (int, Bundle, this).
I had the same problem, when after rotating screen onLoadFinished not called.
I've seen a lot of answers but nothing really fix the issue.
The only thing that helps is to simply add call to getLoaderManager() in onCreate method (it is called again after activity is recreated because of rotation).
It's really fixed the issue for me:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LoaderManager loaderMngr = getLoaderManager();
Log.d(LOG_TAG, "" + loaderMngr);
And a little note about why I didn't just placed initLoader or restartLoader here: because Loader in my case is triggered after another event happens, so this was the only way to solve this problem for me.
Originally idea got from here.
i've been constantly frustrated by this and i can't find a good answer, so hoping someone here can offer guidance.
i have a fragment that uses AsyncTask quite extensively. i'm constantly plagued by bugs where the fragment calls getActivity(), which returns null. i assume these are happening because some method in the fragment are invoked before the activity is attached, or after it is detached.
what's the correct way to handle this in my code? i don't want to have this idiom littered all over the place,
Activity activity = getActivity();
if (activity != null) { // do something }
looking at the docs for Fragment, i can come up with many possible hooks to solve this: isDetached(), onActivityCreated(), onAttach(), isResumed(), and so on. what is the right combination?
EDIT:
A few people have suggested canceling tasks when paused, but this implies that the standard idiom,
new AsyncTask<...>.execute();
cannot be used. It implies that every exec'd AsyncTask needs to be tracked to completion, or canceled. I have simply never seen that in example code from Google or elsewhere. Something like,
private final Set<AsyncTask<?>> tasks = new HashSet<>;
...
AsyncTask<?> t = new AsyncTask<...>() {
...
public void onPostExecute(...) {
tasks.remove(this);
...
}
}
tasks.add(t);
t.execute();
...
#Override
public void onPause() {
for (AsyncTask<?> t: tasks) {
t.cancel();
}
tasks.clear();
}
Try to cancel your AsyncTasks in the onPause or onStop methods. That will prevent the onPostExecute from being called when the Fragment is not active anymore (getActivity() returns null).
Or you could check if the Fragment is attached by calling this.isAdded() in your Fragment.
I could not find a good solution. In summary, either use a Loader, or check that getActivity() does not return null before using it. I looked into using Loaders, but the pattern makes a lot of assumptions about the structure of the app and the nature of data retrieval that didn't work for me.
In terms of coordinating lifecycles, I keep onActivityCreated as a mental benchmark - it marks the point at which the underlying activity has finished its own onCreate. Prior to that I do not believe there is an activity to getActivity() from.
That get activity is returning null sounds like either you're calling getActivity() too early (i.e. before it is created) or too late (i.e. when it stopped interacting with the fragment). Stopping your tasks in onPause() would prevent getActivity from returning null since it would cut off the task once the fragment stopped interacting with the underlying activity becuase the activity itself was paused. I think waiting for onStop() may be too late since, if the task were to still be running when the underlying activity paused it may still reutrn null.
Using android-support-v4.jar and FragmentActivity (no fragments at this point)
I have an AsyncTaskLoader which I start loading and then change the orientation while the background thread is still running. In my logs I see the responses come through to the background requests. The responses complete and I expect onLoadFinished() to be called, but it never is.
As a means of troubleshooting, in the Manifest, if I set android:configChanges="orientation" onLoadFinished() gets called as expected.
My Activity implements the loader callbacks. In the source for LoaderManager.initLoader() I see that if the loader already exists, the new callback is set to the LoaderInfo inner object class but I don't see where Loader.registerListener() is called again. registerListener only seems to be called when LoaderManagerImpl.createAndInstallLoader() is called.
I suspect that since the activity is destroyed and recreated on orientation change and since it is the listener for callbacks, the new activity is not registered to be notified.
Can anyone confirm my understanding and what the solution so that onLoadFinished is called after orientation change?
Nikolay identified the issue - Thank you.
I was calling initLoader fron onResume(). The Android documentation states:
"You typically initialize a Loader within the activity's onCreate()
method, or within the fragment's onActivityCreated() method."
Read "typically" as a bit more emphatic than I did when it comes to dealing with configuration change life cycle.
I moved my initLoader call to onCreate() and that solved my problem.
I think the reason is that in FragmentActivity.onCreate() a collection of LoaderManagers is pulled from LastNonConfigurationInstance and in FragmentActivity.onStart() there is some start up work regarding Loaders and LoaderManagers. Things are already in process by the time onResume() is called. When the Loader needs instantiated for the first time, calling initLoader from outside onCreate() still works.
It's actually not the call to initLoader() in onCreate() that's fixing it. It's the call to getLoaderManager(). In summary, what happens is that when an activity is restarted, it already knows about the loaders. It tries to restart them when your activity hits onStart(), but then it hits this code in FragmentHostCallback.doLoaderStart()*:
void doLoaderStart() {
if (mLoadersStarted) {
return;
}
mLoadersStarted = true;
if (mLoaderManager != null) {
mLoaderManager.doStart();
} else if (!mCheckedForLoaderManager) {
mLoaderManager = getLoaderManager("(root)", mLoadersStarted, false);
// WTF: Why aren't we calling doStart() here?!
}
mCheckedForLoaderManager = true;
}
Since getLoaderManager() wasn't called yet, mLoaderManager is null. It therefore skips the first condition and the call to mLoaderManager.doStart().
You can test this by simply putting a call to getLoaderManager() in onCreate(). You don't need to call init / restart loaders there.
This really seems like a bug to me.
* This is the code path even if you aren't using fragments, so don't get confused by that.