[Android]非同期処理中にプログレスダイアログを表示(画面回転対応)

投稿者:

非同期処理を実行している間ダイアログを開いて「しばらくお待ちください」などのメッセージやプログレスバーなどで進捗状況を表示したい時がある。

AsyncTaskクラスには、別スレッドで実行する非同期処理のほかに、メインスレッドで実行する前処理や後処理を記述できる。前処理でダイアログを表示して、後処理でダイアログ閉じれば上記の処理を実現できるのではないだろうか。

レイアウトファイル
dialog_progress.xml

まずは表示するダイアログを定義する

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp">

    <ProgressBar
        android:id="@+id/progress_ring"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toEndOf="@+id/progress_ring"
        android:layout_centerVertical="true"
        android:layout_marginStart="10dp"
        android:text="しばらくお待ちください" />
</RelativeLayout>

JAVAファイル
MyProgressDialog.java
public class MyProgressDialog extends DialogFragment {

    public MyProgressDialog(){} //空のコンストラクタ(DialogFragmentのお約束)

    //インスタンス作成
    public static MyProgressDialog newInstance() {
        return new MyProgressDialog();
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Dialog dialog = new Dialog(getActivity());
        dialog.setContentView(R.layout.dialog_progress);
        dialog.setCancelable(false);
        dialog.setTitle("更新中");
        return dialog;
    }
}

次にAsyncTaskを継承した非同期処理クラスを定義する

JAVAファイル
AsyncWait10Seconds.java
public class AsyncWait10Seconds extends AsyncTask<Void,Void,Void> {
    //メンバ変数
    private Activity activity;
    private MyProgressDialog pd = null;

    //コンストラクタ
    public AsyncWait10Seconds(Activity activity) {
        this.activity = activity;
    }

    @Override
    protected void onPreExecute() {
        pd = MyProgressDialog.newInstance();
        pd.show(activity.getFragmentManager(),"TEST"); //ダイアログを開く
    }

    @Override
    protected Void doInBackground(Void...voids) {
        try {
            Thread.sleep(10 * 1000); //10秒待機
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        if (pd != null && pd.getShowsDialog()) {
            pd.dismiss(); //ダイアログを閉じる
        }
    }

}

最後にメインアクティビティを定義

レイアウトファイル
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="Button"/>
</LinearLayout>

JAVAファイル
MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button1 = findViewById(R.id.button);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Test();
            }
        });
    }

    private void Test() {
        AsyncWait10Seconds task = new AsyncWait10Seconds(this);
        task.execute();
    }
}

アプリを実行するとボタン一個だけの画面が表示され、そのボタンをクリックするとダイアログが起動し「しばらくお待ちください」と表示されプログレスサークルがクルクル回るはずだ。そして非同期処理が実行され(この例では10秒待機するだけ)、それが終了するとダイアログが閉じる。

なんだ簡単じゃん、と思ったらそうは問屋が卸さない。ダイアログ表示中にユーザーがある操作をするとこのアプリはたちまちエラーを吐き出す。
その操作とは……

画面の縦横切り替えである

縦から横、あるいは、横から縦に画面の方向を切り替えるとダイアログを閉じるときにNullPointerの例外が発生する。
何故かというと、Androidは画面の方向が変わった時、アクティビティをいったん破棄し、再作成する仕様だからだ。ダイアログも同様で、例ではpdという変数にダイアログのインスタンスを格納しているが、回転によってダイアログが破棄されると変数が参照しているオブジェクトが消滅して機能しなくなる(nullを返す)のである。もちろんAndroidシステムは再作成されたオブジェクトを変数に再格納するなんて気の利いたことはしてくれない。

この問題についてネットを検索すると様々な対策がヒットする

  • そもそも画面を回転させない(画面方向を固定)
  • 回転してもアクティビティを再作成させない(マニフェストで設定できる)
  • コールバックを巧みに利用する

等々……いろんな人が苦心しておられるが、要するに非同期処理が終わったことをダイアログのインスタンスに通知できればいいわけで、通知を受け取ったダイアログが自分自身を閉じればいい話である。

Androidにはいつ起こるかわからないイベントを待ち受ける仕組みとしてブロードキャストがあるが、これを利用して画面の回転でエラーの出ない進捗ダイアログを作ってみよう。

画面回転対応

ダイアログの定義
レイアウトは省略
MyProgressDialog.java
public class MyProgressDialog extends DialogFragment {

    public MyProgressDialog(){} //空のコンストラクタ(DialogFragmentのお約束)

    //インスタンス作成
    public static MyProgressDialog newInstance() {
        return new MyProgressDialog();
    }

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

        //ブロードキャストを受信するように登録
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(MainActivity.ACTION_FINISH_UPDATING);
        getActivity().registerReceiver(br, intentFilter);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Dialog dialog = new Dialog(getActivity());
        dialog.setContentView(R.layout.dialog_progress);
        dialog.setCancelable(false);
        dialog.setTitle("更新中");
        return dialog;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        getActivity().unregisterReceiver(br);   //ブロードキャストの登録解除
    }

    //ダイアログを閉じる
    private void closeDialog() {
        dismissAllowingStateLoss(); //ダイアログを強制的に閉じる
    }

    //ブロードキャストレシーバー
    private BroadcastReceiver br = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action!=null && action.equals(MainActivity.ACTION_FINISH_UPDATING)) {
                closeDialog();
            }
        }
    };
}

メインアクティビティと非同期処理の定義

AsyncTask継承クラスにコンストラクタ引数でActivityクラスを渡すやり方も実はよろしくない。画面回転時にアクティビティも破棄→再作成となるので変数activityも無効になってしまうからだ。なので非同期処理はThreadクラスを継承したインナークラスに変更する。ダイアログを開く等の前処理、後処理もMainActivityの内部に書いてしまう。

レイアウトは省略
MainActivity.java
public class MainActivity extends AppCompatActivity {
    //定数
    final static public String ACTION_FINISH_UPDATING = "ACTION_ASYNC_FINISH_UPDATING";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button1 = findViewById(R.id.button);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Test();
            }
        });
    }

    private void Test() {
        MyProgressDialog pd = MyProgressDialog.newInstance();
        pd.show(getFragmentManager(),"TEST");   //ダイアログを開く
        ThreadWait10Second task = new ThreadWait10Second();
        task.start();   //非同期処理を開始
    }

    //プログレスダイアログを閉じる
    private void closeDialog () {
        //非同期処理が終わったことを通知
        Intent intent = new Intent();
        intent.setAction(ACTION_FINISH_UPDATING);   
        sendBroadcast(intent);
    }

    //非同期処理(インナークラス)
    class ThreadWait10Second extends Thread {

        @Override
        public void run() {
            //super.run();
            try {
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Handler handler = new Handler(Looper.getMainLooper());
            handler.post(new Runnable() {
                @Override
                public void run() {
                    //後処理
                    closeDialog();
                }
            });
        }
    }
}

ダイアログのonCreateメソッドでブロードキャストの受信登録をし、非同期処理の後処理で処理が終わったことをブロードキャストに通知。そして通知を受け取ったダイアログは自身を閉じる。onDestroyでブロードキャストの解除も忘れずに。

これで画面の回転を気にせずにダイアログを表示できるはずだ。通知を飛ばす際にIntentに進捗状況のパラメータを入れておけば「○○%完了」みたいな表示も可能だ。いろいろ応用してみよう。

1件のコメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください