Ventis Ventis - 15 days ago 6
Android Question

Showing a DialogFragment throws "Can not perform this action after onSaveInstanceState" error

Problem



Hi, I'm creating an Android and iOS module for Titanium which has a sendLog method which sends some arbitrary JSON data to a server and returns an URL if it matches some predefined filters. The URL should be opened in a modal Dialog with a webview.

I've written native iOS and Android libraries and wrapped them as Titanium modules. On iOS everything works fine but on Android I can't open the Dialog (see error stack trace below). Right now there's one log message that always triggers the same webpage for testing purposes. On Android it just fails silently.

Test case



var mupets = require("be.iminds.mupets");
mupets.initialize("wappr", "http://tocker.iminds.be:3000/log/report.json", 1, 100, 3);
var esmLog = {
bar: "foo"
};
mupets.sendLog("es-test-01",JSON.stringify(esmLog));


After this piece of code (after about 10 seconds max), the module should show a native dialog with the following webpage inside: http://tocker.iminds.be:3000/es/sheets/test-01/index.html

Instead, this is the error I get consistently:

Logs



java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1411)
at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1429)
at android.app.BackStackRecord.commitInternal(BackStackRecord.java:687)
at android.app.BackStackRecord.commit(BackStackRecord.java:663)
at android.app.DialogFragment.show(DialogFragment.java:256)
at be.iminds.mupets_client_android.logging.plugins.OutHttp.getEsm(OutHttp.java:122)
at be.iminds.mupets_client_android.logging.plugins.OutHttp$1.success(OutHttp.java:78)
at be.iminds.mupets_client_android.HttpClient$1$1.onResponse(HttpClient.java:76)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:133)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)


Here is the Android code that's causing the error:

Activity activity = (Activity) context;
EsmDialogFragment esmDialogFragment = EsmDialogFragment.newInstance(new EsmDialogListener() {
@Override
public void submit(String type, JsonObject result) {
Mupets.sendLog(type, result);
esmShown = false;
}

@Override
public void onCancel(JsonObject cancelled) {
super.onCancel(cancelled);
Mupets.sendLog("ESM_cancelled", cancelled);
esmShown = false;
}
}, url, true);
FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
Fragment prev = activity.getFragmentManager().findFragmentByTag(EsmDialogFragment.ESM_DIALOG_FRAGMENT);
if (prev != null) {
transaction.remove(prev);
}
transaction.addToBackStack(null);
Log.v(TAG, "Pre-show fragment");
esmDialogFragment.show(transaction, EsmDialogFragment.ESM_DIALOG_FRAGMENT);
Log.v(TAG, "Post-show fragment");


Does Titanium either not allow the use of Fragments/or require you to call Dialog.show() at a specific point? The error talks about "...after onSaveInstanceState" but I don't see how I would call it before onSaveInstanceState if I'm not creating an activity and why the code works when I use it in a native Android app.

This is an Titanium example project with the module that should show the dialog after it opens:
https://www.dropbox.com/s/0v77xd5gllv6kb3/testModule.zip?dl=1

Answer

This is not a trivial problem, so there's no quick and easy fix that you can just copy/paste from an answer. The bottom line is that you will have to refactor some of your code.

You are trying to show a DialogFragment in response to an asynchronous operation - if that operation completes after onSaveInstanceState, the callback will try to show the dialog and induce an IllegalStateException.

The way to protect yourself from this issue is to not do UI stuff directly from callbacks. Instead, you need to hold off until you have a started or resumed Activity or Fragment, so that you can show the dialog safely.

An easy way to do this is to use sticky events, i.e. post a sticky event from your callback and subscribe for that type of sticky event in your UI component's onResume method.

If you don't want to use an event bus library, you can instead call your asynchronous method from a non-UI component that updates its internal state inside the callback, and then make the UI component check that state in onResume. If you use this approach, you need to be careful with managing your global state.

Comments