Why is onLoadFinished called again after fragment resumed?
Asked Answered
H

2

14

I have a peculiar issue with Loaders. Currently I am unsure if this is a bug in my code or I misunderstand loaders.

The app

The issue arises with conversations (imagine something similar to Whatsapp). The loaders I use are implemented based on the AsyncTaskLoader example. I am using the support library.

  • In OnCreate, I start a loader to retrieve cached messages.
  • When the CachedMessageLoader finishes, it starts a RefreshLoader to retrieve (online) the newest messages.
  • Each loader type as a distinct ID (say, offline:1 online:2)

This works very well, with the following exception.

Problem

When I open another fragment (and add the transaction to the backstack) and then use the Back-Key to go back to the conversationFragment, onLoadFinished is called again with both results from before. This call happens before the fragment has had any chance to start a loader again...

This delivering of "old" results that I obtained before results in duplicated messages.

Question

  • Why are those results delivered again?
  • Do I use these loaders wrong?
  • Can I "invalidate" the results to ensure that I only get them delivered once or do I have to eliminate duplicates myself?

Stack trace of call

MyFragment.onLoadFinished(Loader, Result) line: 369 
MyFragment.onLoadFinished(Loader, Object) line: 1   
LoaderManagerImpl$LoaderInfo.callOnLoadFinished(Loader, Object) line: 427   
LoaderManagerImpl$LoaderInfo.reportStart() line: 307    
LoaderManagerImpl.doReportStart() line: 768 
MyFragment(Fragment).performStart() line: 1511  
FragmentManagerImpl.moveToState(Fragment, int, int, int, boolean) line: 957 
FragmentManagerImpl.moveToState(int, int, int, boolean) line: 1104  
BackStackRecord.popFromBackStack(boolean) line: 764 
...

Update 1 The loaders mentioned here are initiated by the conversation fragment:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setHasOptionsMenu(true);
    Bundle args = getArguments();
    m_profileId = args.getString(ArgumentConstants.ARG_USERID);
    m_adapter = new MessageAdapter(this);

    if (savedInstanceState != null) {
        restoreInstanceState(savedInstanceState);
    }
    if (m_adapter.isEmpty()) {
        Bundle bundle = new Bundle();
        bundle.putString(ArgumentConstants.ARG_USERID, m_profileId);
        getLoaderManager().restartLoader(R.id.loader_message_initial, bundle, this);
    } else {
        // Omitted: Some arguments passed in Bundle
        Bundle b = new Bundle(). 
        getLoaderManager().restartLoader(R.id.loader_message_refresh, b, this);
    }
}

@Override
public void onResume() {
    super.onResume();
    // Omitted: setting up UI state / initiating other loaders that work fine
}

@Override
public AbstractMessageLoader onCreateLoader(final int type, final Bundle bundle) {
    final SherlockFragmentActivity context = getSherlockActivity();
    context.setProgressBarIndeterminateVisibility(true);
    switch (type) {
        case R.id.loader_message_empty:
            return new EmptyOnlineLoader(context, bundle);
        case R.id.loader_message_initial:
            return new InitialDBMessageLoader(context, bundle);
        case R.id.loader_message_moreoldDB:
            return new OlderMessageDBLoader(context, bundle);
        case R.id.loader_message_moreoldOnline:
            return new OlderMessageOnlineLoader(context, bundle);
        case R.id.loader_message_send:
            sendPreActions();
            return new SendMessageLoader(context, bundle);
        case R.id.loader_message_refresh:
            return new RefreshMessageLoader(context, bundle);
        default:
            throw new UnsupportedOperationException("Unknown loader");
    }
}

@Override
public void onLoadFinished(Loader<Holder<MessageResult>> loader, Holder<MessageResult> holder) {
    if (getSherlockActivity() != null) {
        getSherlockActivity().setProgressBarIndeterminateVisibility(false);
    }
    // Omitted: Error handling of result (can contain exception)
    List<PrivateMessage> unreadMessages = res.getUnreadMessages();
    switch (type) {
        case R.id.loader_message_moreoldDB: {
            // Omitted error handling (no data)
            if (unreadMessages.isEmpty()) {
                m_hasNoMoreCached = true;
                // Launch an online loader
                Bundle b = new Bundle();
                // Arguments omitted
                getLoaderManager().restartLoader(R.id.loader_message_moreoldOnline, b, ConversationFragment.this);
            }
            // Omitted: Inserting results into adapter
        }
        case R.id.loader_message_empty: { // Online load when nothing in DB
            // Omitted: error/result handling handling
            break;
        }
        case R.id.loader_message_initial: { // Latest from DB, when opening
            // Omitted: Error/result handling

            // If we found nothing, request online
            if (unreadMessages.isEmpty()) {
                 Bundle b = new Bundle();
                 // Omitted: arguments
                 getLoaderManager().restartLoader(R.id.loader_message_empty, b, this);
             } else {
                // Just get new stuff
                Bundle b = new Bundle();
               // Omitted: Arguments
               getLoaderManager().restartLoader(R.id.loader_message_refresh, b, this);
            }
            break;
        }
        // Omitted: Loaders that do not start other loaders, but only add returned data to the adapter
        default:
            throw new IllegalArgumentException("Unknown loader type " + type);
    }
    // Omitted: Refreshing UI elements
}

@Override
public void onLoaderReset(Loader<Holder<MessageResult>> arg0) { }

Update 2 My MainActivity (which ultimatively hosts all fragments) subclasses SherlockFragmentActivity and basically launches fragments like this:

    Fragment f = new ConversationFragment(); // Setup omitted
    f.setRetainInstance(false);
    // Omitted: Code related to navigation drawer
    FragmentManager fragmentManager = getSupportFragmentManager();
    fragmentManager.beginTransaction().replace(R.id.fragment_container_frame, f).commit();

The conversation fragment starts the "display profile" fragment like this:

DisplayProfileFragment f = new DisplayProfileFragment();
// Arguments omitted
FragmentManager manager = getSherlockActivity().getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.fragment_container_frame, f).addToBackStack(null).commit();
Hem answered 9/1, 2014 at 21:36 Comment(2)
Could you add the code of the Activity which manipulates the fragments and loaders?Effeminate
Hi @Effeminate Thanks for your input. I added the thinned down version of the code in my fragment which initiates the loaders... I'll try to add the code related to starting fragments in a minute or so.Hem
D
8

There are other similar questions such as Android: LoaderCallbacks.OnLoadFinished called twice However the behavior of the loader manager hooks are what they are. You can either destroy the loader after getting the first set of results

public abstract void destroyLoader (int id)

or you can handle the onLoaderReset and tie your UI data more closely to the loader data

public abstract void onLoaderReset (Loader<D> loader)

Called when a previously created loader is being reset, and thus making its data unavailable. The application should at this point remove any references it has to the Loader's data.

Personally, I would use a ContentProvider and a CursorLoader for this (each row of data would need to have a unique _ID but for messages that should not be a problem).

Dormitory answered 28/1, 2014 at 23:28 Comment(4)
Could you elaborate on the behaviour of these callbacks? The documentation only states that onLoadFinished is "called when a previously created loader has finished its load." - but not that this can occur arbitrarily often or (additionally) at which point in the lifecycle. Do I interpret your answer correctly that (if I use a loader polling new data from a webservice) I'll have to destroy the loader explicitly once I have consumed (and stored) the data?Hem
The documentation also states "This function is guaranteed to be called prior to the release of the last data that was supplied for this Loader." During onResume it is perfectly reasonable that the loader releases its data and reloads during onResume. Yes, if you are seeing a behavior where the loader may callback and you don't want that callback, then destroy the loader.Dormitory
Calling destroyLoader does fix my issue, thx. You made me aware that I was not quite as familiar with those callbacks as I thought.Hem
calling destroyLoader() is also destroying the cursor, Is there is a way to destroy loader but keep cursor ??Aquanaut
D
0

use following this in onResume()

 @Override
public void onResume() {
    super.onResume();
    getLoaderManager().initLoader(0, null, this);
}

i solved my problem from this this is same type Q

Dalia answered 3/7, 2020 at 10:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.