【Androidアプリ開発】意外と難しいプログレスダイアログの画面回転対応 ~別プロセスのServiceの進行状況表示~

こんにちは。ウェブテクノロジの清水です。

Androidアプリで時間がかかる処理を行っているときは、なにかアニメーションを表示させるか、プログレスバーなどを表示するのが一般的です。ごくあたりまえの処理ですが、特殊な構成のアプリでは、意外とやっかいな問題が発生することがわかりました。

今回は、別プロセスのServiceで動かしている処理の進行状況をプログレスダイアログを使用して表示させようとした場合に、画面回転への対応で実際に発生して困った問題点と、その対策についての試行錯誤の過程、解決方法をご紹介します。

1.端末を回転すると落ちる!

図1のような構造のAndroidアプリを開発した際に、別プロセスのServiceのメインスレッドで実装した計算処理にけっこう時間がかかる(数秒~数十秒)ため、進行状況を表示するためにProgressDialogクラスでプログレスバーを表示させることにしました。

プログラム構造

図1:プログラム構造

普通にProgressDialogクラスを使用したコードでも一応は動いていたのですが、プログレスダイアログが表示されているときに端末を回転させると、プログレスダイアログが消えてしまい、しかも処理が終わった時点でアプリが落ちてしまいました。

このときのプログレスダイアログを使ったコードは、ネット上などでもよく紹介されている一般的なサンプルプログラムと同程度のものでした。しかし、これだけでは端末の回転対応は不完全だったのです。

2.画面回転対応で一苦労

アプリ自体を画面回転に非対応にすれば問題は回避できますが、もともと画面回転対応を前提に開発していたアプリなのでそうもいきません。ちゃんと画面回転に対応することにしました。

画面回転時のプログレスダイアログの問題には多くのアプリ開発者が遭遇しているようで、ネット上でも解決方法が数多く紹介されています。しかし、そのほとんどがAsyncTaskとProgressDialogの組み合わせによるものでした。

今回のような『別プロセスのServiceとプロセス間通信をしながら、その別プロセスのServiceの進行状況をプログレスダイアログで表示する場合』の画面回転対応の方法、などというレアケース(?)を取り上げている情報は簡単には見つかりません。

また、AsyncTaskとProgressDialogの画面回転対応のサンプルプログラムを参考にして色々と対策を入れてはみたのですが、今回のプロセス間通信を行っているアプリの構造に合わないためか、なかなか正常に動作しませんでした。

たとえば、

  • プログレスダイアログが表示されている間に、1回だけ画面回転させると正常に動くのに、2回以上行うとダメ
  • 画面回転後のプログレスダイアログの再表示と処理終了のタイミングが重なると、プログレスダイアログが出たまま消えなくなる
  • 画面上ではダイアログは消えているのに、isShowing()でダイアログの表示状態を調べると表示状態のままということになっている

といったような問題が発生して困りました。

さらに、ネット上の情報は、低いAPIレベルを前提にした古いものが多く、現在開発中のアプリの動作対象(APIレベル15)とは動作に違いが発生して解決に余計時間がかかったこともありました。

AsyncTaskとProgressDialogなど、同一プロセス内の別スレッドの処理との協調動作はstaticクラスでプログレスダイアログを管理するなどの方法が使えるので難しくないかもしれませんが、今回のような別プロセスのServiceとの協調が必要になるケースでは、プログレスダイアログの制御を計算処理側から簡単に行うことができません。

というわけでここからは、今回試行錯誤した結果、上手くいった対策についてご紹介します。

3. APIレベル13以上ならば指定追加が必要

調査していてまずわかったことです。

画面回転対策として、AndroidManifest.xmlで

android:configChanges="orientation|keyboardHidden"

を指定する方法はよく知られていますが、APIレベル13 (Android 3.2) 以上を動作対象にした場合は、

android:configChanges="orientation|screenSize|keyboardHidden"

のように、screenSizeの指定も追加しておく必要があります。

screenSizeを追加し忘れていると、画面回転時にonConfigurationChanged()が呼び出されないため、画面回転対応がうまくできない原因になります。

今回のアプリでは動作対象のAPIレベルを15 (Android 4.0.3) 以上にしていたため、この部分でもつまづきました。developer.android.comなどで常に最新情報をチェックしていればすぐに気付くことかもしれませんが、多くの開発者にとってはなかなか気付かないポイントではないでしょうか。

4. 時間がかかる処理の側からダイアログを制御

今回は色々と試行錯誤をした結果、別プロセスのServiceの方で、計算処理中に別スレッドから定期的に(今回のアプリでは0.2秒毎に)本体アプリ側にメッセージを投げてもらうことにしました。本体アプリは、必要に応じてプログレスダイアログの更新や端末の回転で消えたプログレスダイアログの再表示を行い、処理終了時にプログレスダイアログを消します。

具体的には、アプリ本体側では、HandlerのhandleMessage()でServiceからのメッセージを待ち受けて、プログレスダイアログが表示されていればsetProgress()で進捗状況の更新を行っています。

プログレスダイアログの表示開始処理を行う場所

  • Serviceの処理を起動する直前
  • handleMessage()内で、Serviceからメッセージを受けた時にプログレスダイアログが表示されていなかった場合

画面回転時はプログレスダイアログが自動的に消えて自動再表示はされませんが、その時は画面の表示状態とProgressDialogクラスのオブジェクトの状態が一致しない状態になっており、そのままプログレスダイアログを操作しようとすると簡単に落ちてしまいます。そこで、端末の回転などによってプログレスダイアログが消えた場合と、プログレスダイアログを消す必要がある場合の両方の場所に明示的にdismiss()とnull代入を入れました。

プログレスダイアログを消す処理を行う場所

  • handleMessage()内で、Serviceから処理完了のメッセージを受け取った部分
  • onConfigurationChanged()内
  • onPause()内

これで、画面回転時は、

自動的にプログレスダイアログが消える

onConfigurationChanged()でプログレスダイアログのdismiss()

その後、Service側からのメッセージが届いたらプログレスダイアログを再表示

という動作になります。

Service側の計算処理中には、Service側から高頻度でメッセージが届くようにしてあるので、計算処理中であれば、端末の回転を行ってもほぼ瞬時にプログレスダイアログが再表示されます。

5. replyToをnullにしたMessageを送信する

最後にもう一つ、問題が残っていました。

今回のアプリでは、別プロセスのService側の処理プログラム本体はメインスレッドを完全に占領するブロッキング動作になるので、アプリ本体側から「処理開始」のメッセージを受け取り計算処理を開始したService側では、以降は処理が終了するまでアプリ本体側からのメッセージを受け取ることができません。

このため、通常のMessengerの使い方ではreplyToに自分自身のMessengerを指定して応答メッセージを受け取るので、今回のアプリではService側のメッセージキューに応答メッセージが溜まってしまい、その後はメッセージの送信もできなくなってしまいました。

この問題を回避するために、Service側のTimerTaskのrun()内でMessengerを使用してアプリ本体側にsend()でメッセージを送る部分では、MessageのreplyToをnullにして、片方向の「送りっぱなし」ができる状態にする必要がありました。

実はこれが原因で、プログレスダイアログの画面回転対応などを行うよりもずっと前の初期の段階で、プロセス間通信がうまくできずに悩んでいたのですが、応答メッセージが受け取れない状態なら最初から受け取らなければいい、という単純なことに気付くのにしばらく時間が必要でした。

6. まとめ

というわけで、最後に今回のポイントのまとめです。

  • APIレベル13以上では、AndroidManifest.xml の android:configChanges に screenSize を指定しないと、回転時にonConfigrationChanged()が呼び出されないので注意。
  • 別プロセスの処理進行状況をプログレスダイアログで表示させたい場合は、処理スレッド側から本体アプリにメッセージを投げて、プログレスダイアログの表示を制御するのが良い。
  • ブロッキング状態になっているプロセスからメッセージを投げる場合は、必ず Message の replyTo を null にする。

以上、Androidでアプリケーションを開発されている方々の参考になれば幸いです。

タグ , | 2020/06/16 更新 |