android – Can not perform this action inside of onLoadFinished-ThrowExceptions

Exception or error:

I’m creating a new sample android app with one activity and many fragments.

At first i’m starting the “OverviewFragment” (my Dashboard) which should show some basic informations. My OverviewFragment loads the informations with an “AccountProvider” (ContentProvider). If the database is empty, the OverviewFragment should be replaces with my WelcomeFragment…

Here is some code:

public class OverviewFragment extends BaseFragment
    implements LoaderManager.LoaderCallbacks {

private static final int LOADER_ACCOUNTS = 10;
private static final int LOADER_TRANSACTIONS = 20;

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    getLoaderManager().initLoader(LOADER_ACCOUNTS, null, this);
    //getLoaderManager().initLoader(LOADER_TRANSACTIONS, null, this);
}

@Override
public Loader onCreateLoader(int id, Bundle args) {
    Uri uri = null;
    String[] projection = null;
    String selection = null;
    String[] selectionArgs = null;
    String sortOrder = null;

    switch (id) {
        case LOADER_ACCOUNTS:
            uri = WalletProvider.CONTENT_URI;
            projection = new String[]{DBHelper.ACC_ID, DBHelper.ACC_TITLE};
            sortOrder = DBHelper.ACC_TITLE;
            break;
    }
    CursorLoader cl =  new CursorLoader(getActivity(),
            uri, projection, selection, selectionArgs, sortOrder);
    return cl;
}

@Override
public void onLoadFinished(Loader loader, Object data) {
    switch (loader.getId()) {
        case LOADER_ACCOUNTS:
            bindAccounts((Cursor) data);
            break;
    }

}

@Override
public void onLoaderReset(Loader loader) {

}

private void bindAccounts(Cursor cursor) {
    boolean showCreateWallet = true;


    if (cursor != null && cursor.moveToFirst()) {
        showCreateWallet = false;
    }

    if (showCreateWallet) {
        listener.changeFragment(new WalletCreateFragment());
    }
}

and here my main activity

    @Override
public void changeFragment(Fragment fragmentToLoad) {
    FragmentManager fragmentManager = getSupportFragmentManager();
    fragmentManager.beginTransaction()
            .replace(R.id.container, fragmentToLoad)
            .commit();
}

Now… if i’m starting my app with an empty database i get the error you see in the title..

I know that i should not change the fragment in the onLoadFinished function …. but where can i do it? 😛

Sorry for my english =)

How to solve:

This is a bug of android. To solve this, use a Handler to replace/add/call a Fragment inside of onLoadFinished like this:

final int WHAT = 1;
Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {                    
        if(msg.what == WHAT) changeFragment(fragmentToLoad);            
    }
};
handler.sendEmptyMessage(WHAT);

###

This is not a bug, but it’s not a simple issue. onLoadFinished may run at any time, including after an activity’s state has been saved. The resulting fragment change will then not automatically be saved by the framework. You have to explicitly allow this using commitAllowingStateLoss

In the documentation of onLoadFinished:

Called when a previously created loader has finished its load. Note that normally an application is not allowed to commit fragment transactions while in this call, since it can happen after an activity’s state is saved. See FragmentManager.openTransaction() for further discussion on this.

Quoting Dianne Hackborn from the thread referred to in the comment on the other answer:

Just understand that if the activity’s fragment state has already been saved, your code will need to correctly update it if it is later restarted from the state.

[…]

It’s a bad user experience to show a dialog (or do any other major shift in the UI) as the result of a loader. Here is what you are doing: setting off some operation to run in the background for an in-determinant amount of time, which upon completion may throw something in front of the user yanking them out of whatever they were doing.

My suggestion is to show the user whatever information about the loader result in-line in the same way you would show the data from it.

If you (and your users) are ok with the user experience, you can use commitAllowingStateLoss to update the fragment.

###

Both answers are missing a good solution. It’s true that it’s not a good idea to do something on the onLoadfinished as the activity or fragment could have been destroyed.

It’s also not a good user experience to update the UI after something on a separate thread has changed since they may be about to do an action. But that can be fixed with a refresh button that then updates what the user sees.

However, there are circumstances where you may require to actually update the UI. For instance, if we need to do an action that requires the user to wait after they’ve pressed a next button.

In your example, you extend Callbacks directly by the Fragment so you can create a method on your fragment that allows you to safely execute a UI update like this:

private void update(Runnable runnable) {
    View rootView = getView();
    if (isAdded() && rootView != null) {
        rootView.post(runnable);
    }
}

This checks whether the fragment is in a state that can handle UI updates and runs the command. It can be used like this:

update(new Runnable() {
    @Override
    public void run() {
        bindAccounts((Cursor) data);
    }
})

That said, it’s not a very wise idea to use your fragment as the callbacks since it may leak your fragment in the LoaderManager which lives on the Activity.

Another safe way to handle these would be with broadcast receivers (make sure they are unregistered).

However, the safest option will be to use the newly announced Architecture Components for Android when they are ready for production:

https://developer.android.com/topic/libraries/architecture/index.html

Leave a Reply

Your email address will not be published. Required fields are marked *