Note that there are some explanatory texts on larger screens.

plurals
  1. POAsyncTaskLoader onLoadFinished with a pending task and config change
    primarykey
    data
    text
    <p>I'm trying to use an <code>AsyncTaskLoader</code> to load data in the background to populate a detail view in response to a list item being chosen. I've gotten it mostly working but I'm still having one issue. If I choose a second item in the list and then rotate the device <em>before the load for the first selected item has completed</em>, then the <code>onLoadFinished()</code> call is reporting to the activity being stopped rather than the new activity. This works fine when choosing just a single item and then rotating.</p> <p>Here is the code I'm using. Activity:</p> <pre><code>public final class DemoActivity extends Activity implements NumberListFragment.RowTappedListener, LoaderManager.LoaderCallbacks&lt;String&gt; { private static final AtomicInteger activityCounter = new AtomicInteger(0); private int myActivityId; private ResultFragment resultFragment; private Integer selectedNumber; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); myActivityId = activityCounter.incrementAndGet(); Log.d("DemoActivity", "onCreate for " + myActivityId); setContentView(R.layout.demo); resultFragment = (ResultFragment) getFragmentManager().findFragmentById(R.id.result_fragment); getLoaderManager().initLoader(0, null, this); } @Override protected void onDestroy() { super.onDestroy(); Log.d("DemoActivity", "onDestroy for " + myActivityId); } @Override public void onRowTapped(Integer number) { selectedNumber = number; resultFragment.setResultText("Fetching details for item " + number + "..."); getLoaderManager().restartLoader(0, null, this); } @Override public Loader&lt;String&gt; onCreateLoader(int id, Bundle args) { return new ResultLoader(this, selectedNumber); } @Override public void onLoadFinished(Loader&lt;String&gt; loader, String data) { Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId); resultFragment.setResultText(data); } @Override public void onLoaderReset(Loader&lt;String&gt; loader) { } static final class ResultLoader extends AsyncTaskLoader&lt;String&gt; { private static final Random random = new Random(); private final Integer number; private String result; ResultLoader(Context context, Integer number) { super(context); this.number = number; } @Override public String loadInBackground() { // Simulate expensive Web call try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return "Item " + number + " - Price: $" + random.nextInt(500) + ".00, Number in stock: " + random.nextInt(10000); } @Override public void deliverResult(String data) { if (isReset()) { // An async query came in while the loader is stopped return; } result = data; if (isStarted()) { super.deliverResult(data); } } @Override protected void onStartLoading() { if (result != null) { deliverResult(result); } // Only do a load if we have a source to load from if (number != null) { forceLoad(); } } @Override protected void onStopLoading() { // Attempt to cancel the current load task if possible. cancelLoad(); } @Override protected void onReset() { super.onReset(); // Ensure the loader is stopped onStopLoading(); result = null; } } } </code></pre> <p>List fragment:</p> <pre><code>public final class NumberListFragment extends ListFragment { interface RowTappedListener { void onRowTapped(Integer number); } private RowTappedListener rowTappedListener; @Override public void onAttach(Activity activity) { super.onAttach(activity); rowTappedListener = (RowTappedListener) activity; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ArrayAdapter&lt;Integer&gt; adapter = new ArrayAdapter&lt;Integer&gt;(getActivity(), R.layout.simple_list_item_1, Arrays.asList(1, 2, 3, 4, 5, 6)); setListAdapter(adapter); } @Override public void onListItemClick(ListView l, View v, int position, long id) { ArrayAdapter&lt;Integer&gt; adapter = (ArrayAdapter&lt;Integer&gt;) getListAdapter(); rowTappedListener.onRowTapped(adapter.getItem(position)); } } </code></pre> <p>Result fragment:</p> <pre><code>public final class ResultFragment extends Fragment { private TextView resultLabel; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.result_fragment, container, false); resultLabel = (TextView) root.findViewById(R.id.result_label); if (savedInstanceState != null) { resultLabel.setText(savedInstanceState.getString("labelText", "")); } return root; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("labelText", resultLabel.getText().toString()); } void setResultText(String resultText) { resultLabel.setText(resultText); } } </code></pre> <p>I've been able to get this working using plain <code>AsyncTask</code>s but I'm trying to learn more about <code>Loader</code>s since they handle the configuration changes automatically.</p> <hr> <p><strong>EDIT</strong>: I think I may have tracked down the issue by looking at the source for <a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.3_r1/android/app/LoaderManager.java?av=f#552">LoaderManager</a>. When <code>initLoader</code> is called after the configuration change, the <code>LoaderInfo</code> object has its <code>mCallbacks</code> field updated with the new activity as the implementation of <code>LoaderCallbacks</code>, as I would expect.</p> <pre><code>public &lt;D&gt; Loader&lt;D&gt; initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks&lt;D&gt; callback) { if (mCreatingLoader) { throw new IllegalStateException("Called while creating a loader"); } LoaderInfo info = mLoaders.get(id); if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args); if (info == null) { // Loader doesn't already exist; create. info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks&lt;Object&gt;)callback); if (DEBUG) Log.v(TAG, " Created new loader " + info); } else { if (DEBUG) Log.v(TAG, " Re-using existing loader " + info); info.mCallbacks = (LoaderManager.LoaderCallbacks&lt;Object&gt;)callback; } if (info.mHaveData &amp;&amp; mStarted) { // If the loader has already generated its data, report it now. info.callOnLoadFinished(info.mLoader, info.mData); } return (Loader&lt;D&gt;)info.mLoader; } </code></pre> <p>However, when there is a pending loader, the main <code>LoaderInfo</code> object also has an <code>mPendingLoader</code> field with a reference to a <code>LoaderCallbacks</code> as well, and this object is never updated with the new activity in the <code>mCallbacks</code> field. I would expect to see the code look like this instead:</p> <pre><code>// This line was already there info.mCallbacks = (LoaderManager.LoaderCallbacks&lt;Object&gt;)callback; // This line is not currently there info.mPendingLoader.mCallbacks = (LoaderManager.LoaderCallbacks&lt;Object&gt;)callback; </code></pre> <p>It appears to be because of this that the pending loader calls <code>onLoadFinished</code> on the old activity instance. If I breakpoint in this method and make the call that I feel is missing using the debugger, everything works as I expect.</p> <p>The new question is: Have I found a bug, or is this the expected behavior?</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. COTake a look at the [`CursorLoader.java`](http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/content/CursorLoader.java/?v=source) source code. Try implementing `onStartLoading`, `onStopLoading`, `onCanceled`, `onReset`, and `deliverResult` similar to how the source code does... the `LoaderManager` assumes that all of these methods are implemented correctly. This might be why your implementation is only partially working across configuration changes.
      singulars
    2. COSo it turns out that `onLoadFinished()` *is* actually being called - it's just reporting to the old activity (the one from before the configuration change) rather than to the new one. Question has been edited and the code has been updated.
      singulars
    3. COHonestly, I think the problem here is the triviality of this example itself... in a real life situation the `Loader` wouldn't contain the actual data source (i.e. the `private final int` field is your actual data source, is it not?). `Loader`s are also supposed to monitor their data source and report back when changes are made. In most cases `onLoadFinished` won't be called on your new Activity after a configuration change since the Loader is smart enough to retain its old data... it will only re-load if it sees that changes to its backing data has been made.
      singulars
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload