Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is onLoadFinished called again after fragment resumed?

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();
like image 752
Patrick Avatar asked Jan 09 '14 21:01

Patrick


1 Answers

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).

like image 185
Eric Woodruff Avatar answered Oct 12 '22 07:10

Eric Woodruff