おじゃまぷよ系エンジニアメモ

アプリエンジニアからサーバーとインフラエンジニアに転身しました

DialogFragmentのshowとdismiss方法はコレに落ち着いた

ちょっと仕事でアプリのクラッシュログがすごい量出てきて調べてほしいと言われてとりあえずFabricのスタックトレースをみてみたら
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState」で落ちまくっている。なんとなく予想はしていたが Fragmentの貼り付けているWebViewのonPageStartedでDialogFragmentをshow()して、onPageFinished()でdismiss()している箇所があり
ここの部分が半端無く落ちてる。まぁそうだろうな…WebViewのロードが終わる前に画面回転とかしたらこのExceptionが発生してそのまま即死コース一直線だ

とりあえず参考ソース

シンプルなProgressDialogFragment

public class MyDialogFragment extends DialogFragment {

    private static ProgressDialog progressDialog = null;

    public static MyDialogFragment newInstance(Bundle bundle) {
        MyDialogFragment instance = new MyDialogFragment();
        instance.setArguments(bundle);
        return instance;
    }

    @Override @NonNull
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        if(progressDialog != null){
            return progressDialog;
        }
        progressDialog = new ProgressDialog(getActivity());
        progressDialog.setMessage("通信中");
        progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        return progressDialog;
    }

}

WebViewのonPageStatedでdialogをshow()し、onPageFinished()でdismiss()する

webView.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            return super.shouldOverrideUrlLoading(view, url);
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
            dialog.show(getFragmentManager(),"progress");
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            dialog.dismiss();
        }
});

これはWebViewのonPageFinishedの処理が走る前に画面を回転させたりすると、いつものjava.lang.IllegalStateException: Can not perform this action after onSaveInstanceStateで死ぬのがすぐわかる
とりあえずif(isResumed())で囲って対処してみる

webView.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            return super.shouldOverrideUrlLoading(view, url);
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
            if(isResumed()){
                  dialog.show(getFragmentManager(),"progress");
            }
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            if(isResumed()){
                  dialog.show(getFragmentManager(),"progress");
            }
        }
});

こうすることでjava.lang.IllegalStateException: Can not perform this action after onSaveInstanceStateで落ちることはなくなる。 これで大丈夫だろうと思っていたらdismiss()のところで
java.lang.NullPointerException: Attempt to invoke virtual method 'android.support.v4.app.FragmentTransaction android.support.v4.app.FragmentManager.beginTransaction()' on a null object reference
で落ちる。。。 同じExceptionでググっているとdialog.onDismiss(dialog.getDialog())という記述で安全に消せるらしい。
確かにその落ちなくなったがなんとなくリファレンス見ていると「This method will be invoked when the dialog is dismissed.」dimissされるときに呼び出されると書いてある
本来はDialogInterfaceを引数にcancelされたときやdimissされた時に何かしたいときに使うものなのだろうなぁと思い。 onDismissの動き見てみると

    public void onDismiss(DialogInterface dialog) {
        if (!mViewDestroyed) {
            // Note: we need to use allowStateLoss, because the dialog
            // dispatches this asynchronously so we can receive the call
            // after the activity is paused.  Worst case, when the user comes
            // back to the activity they see the dialog again.
            dismissInternal(true);
        }
    }

ViewがDestroyされてなければ、dismissAllowingStateLoss()してるだけだった…。可能ならばdismissAllowingStateLoss()ではなく普通にdismiss()したい… このonDismiss処理 の「if (!mViewDestroyed)」これがさっきのぬるぽを防ぐ鍵になってるっぽいので
Fragmentが破棄されたタイミングでdialogにnullを入れてifチェックすればいいんじゃないかと思い最終的に

  @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.activity_reservation, container, false);
        dialog = MyDialogFragment.newInstance(null);
        webView = (WebView) view.findViewById(R.id.webView);
        webView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                return super.shouldOverrideUrlLoading(view, url);
            }

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                super.onPageStarted(view, url, favicon);
                if(isResumed()){
                    dialog.show(getFragmentManager(),"progress");
                }
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                if(isResumed() && dialog != null) {
                    dialog.dismiss();
                }
            }
        });
        return view;
    }

    @Override
    public void onDestroyView() {
        dialog = null;
        super.onDestroyView();
    }

この形に落ち着いた。showもdismissもAllowingStateLossを使わずに済んだので満足している。 そもそも特別な理由がない限りWebViewのロード中の表現にDialogを使わないほうがいいと思う。UI的にあまりよくないし
これぐらいならWebViewの上にProgressBarをかぶせてそいつをVISIBLE,INVISIBLEを切り替えたほうがよっぽどエコで変なFragmentの操作でハマることもない

でももしDialogFragmentを表示したい時の備忘録としてなんとなく書いてみた