一部のユーザーは、通知バーでクイックアクションを使用すると、強制的に接近していると報告しています。
"TestDialog"クラスを呼び出す通知にクイックアクションを表示します。 「スヌーズ」ボタンを押した後のTestDialogクラスで、SnoozeDialogを表示します。
private View.OnClickListener btnSnoozeOnClick() {
return new View.OnClickListener() {
public void onClick(View v) {
showSnoozeDialog();
}
};
}
private void showSnoozeDialog() {
FragmentManager fm = getSupportFragmentManager();
SnoozeDialog snoozeDialog = new SnoozeDialog();
snoozeDialog.show(fm, "snooze_dialog");
}
エラーは*IllegalStateException: Can not perform this action after onSaveInstanceState*.
IllegarStateExceptionが発生するコード行は次のとおりです:
snoozeDialog.show(fm, "snooze_dialog");
このクラスは「FragmentActivity」を拡張しており、「SnoozeDialog」クラスは「DialogFragment」を拡張しています。
エラーの完全なスタックトレースは次のとおりです
Java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at Android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.Java:1327)
at Android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.Java:1338)
at Android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.Java:595)
at Android.support.v4.app.BackStackRecord.commit(BackStackRecord.Java:574)
at Android.support.v4.app.DialogFragment.show(DialogFragment.Java:127)
at com.test.testing.TestDialog.f(TestDialog.Java:538)
at com.test.testing.TestDialog.e(TestDialog.Java:524)
at com.test.testing.TestDialog.d(TestDialog.Java:519)
at com.test.testing.g.onClick(TestDialog.Java:648)
at Android.view.View.performClick(View.Java:3620)
at Android.view.View$PerformClick.run(View.Java:14292)
at Android.os.Handler.handleCallback(Handler.Java:605)
at Android.os.Handler.dispatchMessage(Handler.Java:92)
at Android.os.Looper.loop(Looper.Java:137)
at Android.app.ActivityThread.main(ActivityThread.Java:4507)
at Java.lang.reflect.Method.invokeNative(Native Method)
at Java.lang.reflect.Method.invoke(Method.Java:511)
at com.Android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.Java:790)
at com.Android.internal.os.ZygoteInit.main(ZygoteInit.Java:557)
at dalvik.system.NativeStart.main(Native Method)
このエラーを再現することはできませんが、多くのエラーレポートを取得しています。
このエラーをどのように修正できますか?
これはよくあることです issue 。 show()をオーバーライドし、DialogFragment拡張クラスで例外を処理することにより、この問題を解決しました。
public class CustomDialogFragment extends DialogFragment {
@Override
public void show(FragmentManager manager, String tag) {
try {
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
} catch (IllegalStateException e) {
Log.d("ABSDIALOGFRAG", "Exception", e);
}
}
}
このメソッドを適用しても、DialogFragment.classの内部フィールドは変更されないことに注意してください。
boolean mDismissed;
boolean mShownByMe;
これにより、予期しない結果が生じる場合があります。 commit()の代わりにcommitAllowingStateLoss()を使用する方が良い
つまり、commit()
の後のshow()
(DialogFragmentの場合はonSaveInstanceState()
)フラグメントです。
Androidは、フラグメントの状態をonSaveInstanceState()
に保存します。したがって、commit()
フラグメント状態の後にonSaveInstanceState()
フラグメントが失われると、.
その結果、アクティビティが強制終了され、後で再作成されても、フラグメントはアクティビティに追加されず、ユーザーエクスペリエンスが低下します。だからこそ、Androidはすべてのコストで州の損失を許可しません。
簡単な解決策は、状態が既に保存されているかどうかを確認することです。
boolean mIsStateAlreadySaved = false;
boolean mPendingShowDialog = false;
@Override
public void onResumeFragments(){
super.onResumeFragments();
mIsStateAlreadySaved = false;
if(mPendingShowDialog){
mPendingShowDialog = false;
showSnoozeDialog();
}
}
@Override
public void onPause() {
super.onPause();
mIsStateAlreadySaved = true;
}
private void showSnoozeDialog() {
if(mIsStateAlreadySaved){
mPendingShowDialog = true;
}else{
FragmentManager fm = getSupportFragmentManager();
SnoozeDialog snoozeDialog = new SnoozeDialog();
snoozeDialog.show(fm, "snooze_dialog");
}
}
注:フラグメントが再開されると、onResumeFragments()が呼び出されます。
private void showSnoozeDialog() {
FragmentManager fm = getSupportFragmentManager();
SnoozeDialog snoozeDialog = new SnoozeDialog();
// snoozeDialog.show(fm, "snooze_dialog");
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(snoozeDialog, "snooze_dialog");
ft.commitAllowingStateLoss();
}
ref: リンク
ダイアログがあまり重要ではない場合(アプリが閉じたとき/表示されなくなったときにダイアログを表示しないようにしてもかまいません)、次を使用します。
boolean running = false;
@Override
public void onStart() {
running = true;
super.onStart();
}
@Override
public void onStop() {
running = false;
super.onStop();
}
そして、実行中にのみダイアログ(フラグメント)を開きます:
if (running) {
yourDialog.show(...);
}
編集、より良い解決策:
ライフサイクルでonSaveInstanceStateが呼び出されるのは予測不能ですが、より良い解決策は次のようにisSavedInstanceStateDone()をチェックすることです。
/**
* True if SavedInstanceState was done, and activity was not restarted or resumed yet.
*/
private boolean savedInstanceStateDone;
@Override
protected void onResume() {
super.onResume();
savedInstanceStateDone = false;
}
@Override
protected void onStart() {
super.onStart();
savedInstanceStateDone = false;
}
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
savedInstanceStateDone = true;
}
/**
* Returns true if SavedInstanceState was done, and activity was not restarted or resumed yet.
*/
public boolean isSavedInstanceStateDone() {
return savedInstanceStateDone;
}
数日後、ソリューションを修正した方法を共有したいと思います。DialogFragmentを表示するには、show()
メソッドをオーバーライドし、Transaction
オブジェクトでcommitAllowingStateLoss()
を呼び出す必要があります。 Kotlinの例を次に示します。
override fun show(manager: FragmentManager?, tag: String?) {
try {
val ft = manager?.beginTransaction()
ft?.add(this, tag)
ft?.commitAllowingStateLoss()
} catch (ignored: IllegalStateException) {
}
}
私は何年もこの問題に遭遇しました。
インターネットにはこれに関する多くの議論(数百?数千?)が散らばっていて、インターネットの混乱と偽情報はたくさんあるようです。
状況を悪化させるために、xkcdの「14標準」コミックの精神で、私はリングに答えを投げ入れています。
cancelPendingInputEvents()
、commitAllowingStateLoss()
、catch (IllegalStateException e)
、および同様のソリューションはすべて凶悪に見えます。
問題を再現して修正する方法を次のように簡単に示してください。
private static final Handler sHandler = new Handler();
private boolean mIsAfterOnSaveInstanceState = true;
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
mIsAfterOnSaveInstanceState = true; // <- To repro, comment out this line
}
@Override
protected void onPostResume()
{
super.onPostResume();
mIsAfterOnSaveInstanceState = false;
}
@Override
protected void onResume()
{
super.onResume();
sHandler.removeCallbacks(test);
}
@Override
protected void onPause()
{
super.onPause();
sHandler.postDelayed(test, 5000);
}
Runnable test = new Runnable()
{
@Override
public void run()
{
if (mIsAfterOnSaveInstanceState)
{
// TODO: Consider saving state so that during or after onPostResume a dialog can be shown with the latest text
return;
}
FragmentManager fm = getSupportFragmentManager();
DialogFragment dialogFragment = (DialogFragment) fm.findFragmentByTag("foo");
if (dialogFragment != null)
{
dialogFragment.dismiss();
}
dialogFragment = GenericPromptSingleButtonDialogFragment.newInstance("title", "message", "button");
dialogFragment.show(fm, "foo");
sHandler.postDelayed(test, 5000);
}
};
fragmentManagerの代わりにFragmentTransactionを使用してみてください。以下のコードが問題を解決すると思います。そうでない場合は、お知らせください。
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
SnoozeDialog snoozeDialog = new SnoozeDialog();
snoozeDialog.show(ft, "snooze_dialog");
編集:
このリンクを確認してください。クエリを解決できると思います。
package Android.support.v4.app; /** * 2017年8月16日にGilによって作成されました。 */ public class StatelessDialogFragment extends DialogFragment { /** *ダイアログを表示し、既存のトランザクションを使用してフラグメントを追加してから、 *状態の損失を許容するトランザクション。
* *ほとんどの場合{@link #show(FragmentTransaction、String)}を使用することをお勧めしますが、 *これは本当に気にしないダイアログ用です。 (デバッグ/追跡/広告など) * * @param transaction *フラグメントを追加する既存のトランザクション。 * @param tag * によるこのフラグメントのタグ* {@link FragmentTransaction#add(Fragment、String)FragmentTransaction.add}。 * @returnコミットされたトランザクションの識別子を返します。 、 * {@link FragmentTransaction#commit()FragmentTransaction.commit()}。 * @see StatelessDialogFragment#showAllowingStateLoss(FragmentManager、String) */ public int showAllowingStateLoss(FragmentTransaction transaction、String tag) { mDismissed = false; mShownByMe = true; transaction.add(this、tag); mViewDestroyed = false; mBackStackId = transaction.commitAllowingStateLoss(); return mBackStackId; } /** *ダイアログを表示し、指定されたFragmentManagerにフラグメントを追加します。これは便利です。 *トランザクションを明示的に作成し、指定されたタグでフラグメントを追加し、 *状態を気にせずにコミットします。これはnotトランザクションを *バックスタックに追加します。フラグメントが破棄されると、アクティビティからフラグメントを削除するために新しいトランザクションが実行されます。 *.
* *ほとんどの場合{@link #show(FragmentManager、String)}を使用することをお勧めしますが、これは *本当に気にしないダイアログの場合です。 (デバッグ/追跡/広告など) * * * @param manager *このフラグメントが追加されるFragmentManager。 * @param tag * によるこのフラグメントのタグ* {@link FragmentTransaction#add(Fragment、String)FragmentTransaction.add}。 * @see StatelessDialogFragment# showAllowingStateLoss(FragmentTransaction、String) */ public void showAllowingStateLoss(FragmentManager manager、String tag) { mDismissed = false; mShownByMe = true; FragmentTransaction ft = manager.beginTransaction(); ft.add(this、tag); ft.commitAllowingStateLoss(); } }
Showの代わりにメソッドshowAllowingStateLossを使用します
楽しい ;)
多くのビューは、クリックハンドラーなどの高レベルイベントをイベントキューにポストし、遅延実行します。したがって、問題は、アクティビティに対して「onSaveInstanceState」がすでに呼び出されているが、イベントキューに遅延「クリックイベント」が含まれていることです。したがって、このイベントがハンドラーにディスパッチされると
at Android.os.Handler.handleCallback(Handler.Java:605)
at Android.os.Handler.dispatchMessage(Handler.Java:92)
at Android.os.Looper.loop(Looper.Java:137)
そしてあなたのコードがshow
IllegalStateExceptionがスローされます。
最も簡単な解決策は、onSaveInstanceState
のイベントキューを消去することです
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// ..... do some work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KitKat) {
findViewById(Android.R.id.content).cancelPendingInputEvents();
}
}
公式にはどこにも言及されていませんが、この問題に何度か直面しました。私の経験では、この問題の原因となる古いプラットフォーム上のフラグメントをサポートする互換性ライブラリに何か問題があります。これをテストするには、通常のフラグメントマネージャーAPIを使用します。何も機能しない場合は、ダイアログフラグメントの代わりに通常のダイアログを使用できます。
ダイアログフラグメントオブジェクトをグローバルにし、onPause()メソッドでdismissAllowingStateLoss()を呼び出します
@Override
protected void onPause() {
super.onPause();
if (dialogFragment != null) {
dialogFragment.dismissAllowingStateLoss();
}
}
フラグメントに値を割り当て、ボタンのクリック時またはどこでもshow()を呼び出すことを忘れないでください。
このエラーは、onSaveInstanceState
が呼び出された後に入力イベント(キーダウンイベントやonclickイベントなど)が配信されるために発生しているようです。
解決策は、アクティビティのonSaveInstanceState
をオーバーライドし、保留中のイベントをキャンセルすることです。
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KitKat) {
final View rootView = findViewById(Android.R.id.content);
if (rootView != null) {
rootView.cancelPendingInputEvents();
}
}
}
このコードを使用
FragmentTransaction ft = fm.beginTransaction();
ft.add(yourFragment, "fragment_tag");
ft.commitAllowingStateLoss();
の代わりに
yourFragment.show(fm, "fragment_tag");
以下の実装は、特にダイアログを表示するために、Activity
ライフサイクル中に安全に状態変更を実行する問題を解決するために使用できます。インスタンスの状態がすでに保存されている場合それらは、再開された状態が実行されるまで続きます。
_public abstract class XAppCompatActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/** The retained fragment for this activity */
private ActivityRetainFragment retainFragment;
/** If true the instance state has been saved and we are going to die... */
private boolean instanceStateSaved;
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// get hold of retain Fragment we'll be using
retainFragment = ActivityRetainFragment.get(this, "Fragment-" + this.getClass().getName());
}
@Override
protected void onPostResume() {
super.onPostResume();
// reset instance saved state
instanceStateSaved = false;
// execute all the posted tasks
for (ActivityTask task : retainFragment.tasks) task.exec(this);
retainFragment.tasks.clear();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
instanceStateSaved = true;
}
/**
* Checks if the activity state has been already saved.
* After that event we are no longer allowed to commit fragment transactions.
* @return true if the instance state has been saved
*/
public boolean isInstanceStateSaved() {
return instanceStateSaved;
}
/**
* Posts a task to be executed when the activity state has not yet been saved
* @param task The task to be executed
* @return true if the task executed immediately, false if it has been queued
*/
public final boolean post(ActivityTask task)
{
// execute it immediately if we have not been saved
if (!isInstanceStateSaved()) {
task.exec(this);
return true;
}
// save it for better times
retainFragment.tasks.add(task);
return false;
}
/** Fragment used to retain activity data among re-instantiations */
public static class ActivityRetainFragment extends Fragment {
/**
* Returns the single instance of this fragment, creating it if necessary
* @param activity The Activity performing the request
* @param name The name to be given to the Fragment
* @return The Fragment
*/
public static ActivityRetainFragment get(XAppCompatActivity activity, String name) {
// find the retained fragment on activity restarts
FragmentManager fm = activity.getSupportFragmentManager();
ActivityRetainFragment fragment = (ActivityRetainFragment) fm.findFragmentByTag(name);
// create the fragment and data the first time
if (fragment == null) {
// add the fragment
fragment = new ActivityRetainFragment();
fm.beginTransaction().add(fragment, name).commit();
}
return fragment;
}
/** The queued tasks */
private LinkedList<ActivityTask> tasks = new LinkedList<>();
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
}
/** A task which needs to be performed by the activity when it is "fully operational" */
public interface ActivityTask {
/**
* Executed this task on the specified activity
* @param activity The activity
*/
void exec(XAppCompatActivity activity);
}
}
_
次に、次のようなクラスを使用します。
_/** AppCompatDialogFragment implementing additional compatibility checks */
public abstract class XAppCompatDialogFragment extends AppCompatDialogFragment {
/**
* Shows this dialog as soon as possible
* @param activity The activity to which this dialog belongs to
* @param tag The dialog fragment tag
* @return true if the dialog has been shown immediately, false if the activity state has been saved
* and it is not possible to show it immediately
*/
public boolean showRequest(XAppCompatActivity activity, final String tag) {
return showRequest(activity, tag, null);
}
/**
* Shows this dialog as soon as possible
* @param activity The activity to which this dialog belongs to
* @param tag The dialog fragment tag
* @param args The dialog arguments
* @return true if the dialog has been shown immediately, false if the activity state has been saved
* and it is not possible to show it immediately
*/
public boolean showRequest(XAppCompatActivity activity, final String tag, final Bundle args)
{
return activity.post(new XAppCompatActivity.ActivityTask() {
@Override
public void exec(XAppCompatActivity activity) {
if (args!= null) setArguments(args);
show(activity.getSupportFragmentManager(), tag);
}
});
}
/**
* Dismiss this dialog as soon as possible
* @return true if the dialog has been dismissed immediately, false if the activity state has been saved
* and it is not possible to dismissed it immediately
*/
public boolean dismissRequest()
{
return dismissRequest(null);
}
/**
* Dismiss this dialog as soon as possible
* @param runnable Actions to be performed before dialog dismissal
* @return true if the dialog has been dismissed immediately, false if the activity state has been saved
* and it is not possible to dismissed it immediately
*/
public boolean dismissRequest(final Runnable runnable)
{
// workaround as in rare cases the activity could be null
XAppCompatActivity activity = (XAppCompatActivity)getActivity();
if (activity == null) return false;
// post the dialog dismissal
return activity.post(new XAppCompatActivity.ActivityTask() {
@Override
public void exec(XAppCompatActivity activity) {
if (runnable != null) runnable.run();
dismiss();
}
});
}
}
_
アプリの状態を気にせずにダイアログを安全に表示できます。
_public class TestDialog extends XAppCompatDialogFragment {
private final static String TEST_DIALOG = "TEST_DIALOG";
public static void show(XAppCompatActivity activity) {
new TestDialog().showRequest(activity, TEST_DIALOG);
}
public TestDialog() {}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
return new AlertDialog.Builder(getActivity(), R.style.DialogFragmentTheme /* or null as you prefer */)
.setTitle(R.string.title)
// set all the other parameters you need, e.g. Message, Icon, etc.
).create();
}
}
_
XAppCompatActivity
内からTestDialog.show(this)
を呼び出します。
パラメータを使用してより一般的なダイアログクラスを作成する場合は、show()
メソッドの引数を使用してBundle
に保存し、getArguments()
を使用してそれらを取得できます。 onCreateDialog()
。
アプローチ全体は少し複雑に見えるかもしれませんが、アクティビティとダイアログの2つの基本クラスを作成すると、非常に使いやすく、完全に機能します。同じ問題の影響を受ける可能性がある他のFragment
ベースの操作に使用できます。