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();
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
or you can handle the onLoaderReset and tie your UI data more closely to the loader 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).