View not attached to window manager
例外の発生状況
ProgressDialogをAsyncTaskの中で安易に使用すると、
java.lang.IllegalArgumentException: View not attahced to window manager
という例外が発生することがある。再現する方法は以下の通り、
Avitivityの中でAsyncTaskを使用し、その処理の最初にProgressDialogを作成して表示し、処理が終了したらdismiss()を呼び出して消去する。
ProgressDialogが表示されている間(くるくる回るアイコンの表示中)に、端末の方向を変更する。例えば、OrientationをPortraitからLandscapeに変更する。
- しばらく待つと上記の例外が発生する。
AndroidはConfigurationが変更されると実行中のActivityを破棄し、同じクラスの新たなActivityを生成して表示するのだが、しかし前のActivity(破棄対象のActivity)の上に表示されているProgressDialog、あるいは破棄対象のActivity上で起動されたAsyncTaskのことは「知ったことではない」という態度のようで、放置されてしまっているようだ。
このため、AsyncTaskが終了してProgressDialogのdismiss()が呼び出されると、もはや戻るべきActivityがないせいなのか(このあたりの事情はもちろんよくわからない)、例外が発生してしまう。
この問題の議論状況
あまたに存在するウェブ上のリソースや書籍等は、この問題を放置したままである。「長時間処理を行う場合のサンプル」として公開されているものはどれもこれもこの問題を気にもとめていないものであって、端的に言えばバグっているのである。例えば、以下にProgressDialogを使ったサンプルがあるがこれもバグっている。個人が気ままに書いたものではないだけに、誤りのあるプログラムをサンプルとして掲載している責任は重いだろう。
まともに議論されているものとしては、例えば以下がある。
解決方法
解決方法として示されているものは以下の通り。
orientationの変更をさせない
AndroidManifest.xml中にActivityを定義するとき「android:configChanges="orientation"」を挿入しておく。 こうすると、configChangesの値として記述されたものが起こった時にAndroidはActivityを破棄せず、その代わりにActivityの onConfigurationChanged(Configuration)が呼び出されるとのことである(未確認)。
このようにして、ProgressDialogを表示中に端末の向きが変更された場合でもActivityを破棄せずに、そのまま処理を継続できるというわけであるが、 しかしこれには問題がある。
本来、端末の向きが変更されたらその向き用のレイアウトを使用したいのである。ProgressDialogが消された後でレイアウトを強制的に変更するにはどうすればよいのか?
- そもそも、手軽にConfigurationを変更する方法として端末の回転があるが、例えば将来的にボタン一つでLocaleを変更できるような機能が追加された場合には(他に思いつかないのだが)、このプログラムはやはりクラッシュしてしまう。
Bacon Rank Android App
※上述の議論のHeikki Toivonenさんの方法。
AsyncTaskを作成したら、それをアプリケーション側で保持させる。 Activityが破棄あるいは再生成されたら、AsyncTaskにnullあるいは新たなActivityをセットする。 ProgressDialogではなく、(Window.FEATURE_INDETERMINATE_PROGRESSを使用する(タイトルバーに小さな進行中のアイコンが表示される)。 ProgressDialogでもうまくいくかもしれない。
以下はHeikki Toivonenさんのブログの翻訳
Acitivityのstatic領域としてワークスレッドを保持する
※上述の議論のsamsonsuさんの方法。検討中。
ActivityをDialogとして使う
※上述の議論のruiさんの方法。以下は抄訳。
すべてを試し、何日間も実験したよ。 Activityの回転を拒否することはしたくないんだ。 やりたいことと言えばこうだ。
- プログレスダイアログは動的な情報を表示する。例えば、「サーバに接続中」とか「データをダウンロード中」とか。
- スレッドが重い処理を行い、ダイアログを更新する。
- 最後に、結果をUIに表示する。
問題は、スクリーンが回転すると、本に書かれているすべての解決策は失敗するということなんだ。 こういった状況で「正しい」やり方であるはずのAsyncTaskクラスを使っても。 スクリーンが回転すると、スレッドがやりとりしている「現在のコンテキスト」は消滅してしまう。そして、表示中のダイアログは失敗してしまう。 いかなるトリック(新しいコンテキストを実行中のスレッドに与えようと、スレッド状態を回転中に保持しようと)をコードに追加しようと、問題はいつでもダイアログなんだ。 コードは複雑になるが、必ず失敗してしまうんだ。
うまく行った唯一の解決策はActivity/Dialogトリックだ。
- ダイアログを作成して表示するのではなく、マニフェストでandroid:theme="@android:style/Theme.Dialog"と設定したActivityを作成する。これはダイアログのように見える。
- showDialog(DIALOG_ID)ではなく、startActivityForResult(yourActivityDialog, yourCode)にする。
- Use onActivityResult in the calling Activity to get the results from the executing thread (even the errors) and update the UI.
On your 'ActivityDialog', use threads or AsyncTask to execute long tasks and onRetainNonConfigurationInstance to save "dialog" state when rotating the screen.
これは早いしうまく動く。
This is fast and works fine. I still use dialogs for other tasks and the AsyncTask for something that doesn't require a constant dialog on screen. But with this scenario, I always go for the Activity/Dialog pattern.
And, I didn't try it, but it's even possible to block that Activity/Dialog from rotating, when the thread is running, speeding things up, while allowing the calling Activity to rotate.
ContentViewをすり替える(?)方法
※上述の議論のPzannoさんの方法の抄訳
僕も同じ問題に遭遇した。最終的には、ProgressDialogを使わないことにしたよ。 そのかわりにProgressBarのあるレイアウトを作ることにした。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <ProgressBar android:id="@+id/progressImage" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> </RelativeLayout> そうして、onCreateメソッドの中で以下を行う。 {{{ public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.progress); }
}}}
そうしてスレッドの中で長い処理をさせて、終わったら実際に使いたいレイアウトをActivityにセットする。 例えば、
mHandler.post(new Runnable(){ public void run() { setContentView(R.layout.my_layout); } });
ProgressDialogを表示するより速いし、煩わしくないし、僕が思うに見かけもいいと思うよ。 でも、どうしてもProgressDialogを使いたいならこの方法はボツだね。
私の見解
※ここからは私自身の見解
Activityの中にビジネスロジックを実装しようとするのが根本的な間違いである。 もちろん、これはAndroidに限らず多くのGUI開発環境で行いがちのことではあるのだが。
一つのウインドウクラスを作成してその中にボタンや何やらをつめこみ、ついでにその画面にやらせたいことを一緒に記述してしまう。 例えば、データベースをオープンして、指定された条件で検索し、結果を取得するといったような処理である。
これは基本的には間違いであり、他の多くのGUI環境でも、処理内容・コード量があまりに小さいといった特殊なケース以外は行ってはならない。
しかし、Android環境の場合ではもっとラジカルになる。つまり、絶対に行ってはならない。 なぜなら、Android環境では、進行中のビジネスロジックとは無関係にウインドウ(Activity)がいつ破棄されるかわからないからだ。
ここが従来のGUI環境とは全く異なる。従来のGUI環境であれば、ウインドウが不意に消失してしまうことを想定する必要などないのである。 ウインドウの流れとビジネスロジックの流れを不可分にしてもうまく行ってしまう。
しかし、Androidアプリのビジネスロジックは、Activityとは無関係な流れを形成していなくてはならないのであり、Activityというものはその流れの中のある局面をユーザに表示するためだけの役割でなくてはいけない。あるいは、ビジネスロジック側がユーザ入力を受け付けたい場合にのみ使用される「従」の立場でなければならない。つまり、発想を完全に逆転しなければならないのだ。
アクティビティにはAndroidアプリの中に流れているビジネスロジックの息継ぎ場所の役割しか負わせてはならないのである。
そして、このビジネスロジックの「流れ」をどこに保持すべきであるかと言えば、それはGUIから独立したシングルトンであろう (Applicationに保持させると言う方法も考えられるが、Androidシステムからは切り離した方が何かと都合がよい)。 上記の解決策の中でActivity中にstaticとして保持するといったものがあるが、これは間違った設計である。 なぜなら、これではビジネスロジックとGUIがごちゃまぜになるし、アプリ中の他のビジネスロジックとの連携がとりにくくなってしまう。
Intentを「使わない」こと
Intentを使ってはならない、というと語弊があるが、そのつもりでいなければならない。 同一のアプリ内のアクティビティAからBに画面遷移を行うのにもIntentを発行しなければならないのだが、このIntentの中にBで必要なすべてのデータを格納してしまうといったことをやめる必要がある。
再度、アクティビティとはアプリケーションを流れるビジネスロジックの単なる部分的な表現なのである。Intentを使ってアプリとして重要なデータを渡す必要などはない。単に呼び出されるアクティビティに対して若干のパラメータを与える役割を負わせればよいことである。
これらのIntentによってあたかもアクティビティ間の「ゆるい」結合が可能なように思わされてしまうのだが、これは絵に描いた餅であり、現実のアプリケーションでは全く意味がない。
同一アプリ内の画面遷移であってもIntentが必要な理由は、単に「どこかのアホウがうっかり遷移元のアクティビティの変数で遷移先のアクティビティをつかんでしまったら困る」といった程度の理由しか見つからないが、しかしこういう説明も見当たらない。
※ちなみに、既存の概念を逸脱する新しい概念を作るのであれば、なぜそのようなデザインにしたのかを説明しなければならないと考えるが、Androidにおいてはこれが十分に行われているとは言いがたい。このような状態では「作者が説明できないようなシロモノ」とみなすしかないだろう。
ビジネスロジックを主体とすることのもう一つのメリット
ビジネスロジックをアクティビティから分離独立させる。というよりも、むしろアプリケーションの主体として取り扱うことにより、大きなメリットが生まれる。 それは、Android無しでもテストが可能ということである。また逆に分離されたGUIのみをテスト可能になるということである。
これはむしろ、システム設計として常識的なところのはずである。ビジネスロジックとGUIを明確に分離し、「緩い」インターフェースでのみ接続しておくことにより、 ビジネスロジックはエミュレータ環境無しのEclipse上でテストが可能になるのであり、逆にGUI部分はビジネスロジックのモックを提供することにより、エミュレータ上でテスト可能になるのである。
Androidアプリのあるべき構造
以下はまだ未実証のため、うまく動くかどうかはわからない。
ビジネスロジック側
ビジネスロジック側はandroidのパッケージを一切インポートしないものとする。これによって、android以外の環境でにおいても流用可能となる。 クラスBusinessLogicImplは「現在の状態」を保持しており、この問題の場合には、
- 長い処理の開始前
- 長い処理中
- 長い処理が終了。結果を取得可能
という状態を持つ、長い処理には当然スレッドが使われるが、これももちろんAndroidのHandlerやAsyncTaskは使用しない。 また、少なくとも処理終了時にはイベントを発生することが必要であるが、このイベントを発生させるスレッドとして、
- 任意のスレッド
- AndroidのGUIスレッド
の二つが考えられる。が、これもAndroidパッケージを使用したくないので、「任意のスレッド」とする。 任意のスレッドで発生したイベントをGUIすれっどにて実行するのは、これを受け取るGUIモジュール側とする。
GUI側
GUI側では、BusinessLogicインターフェースのみを保持し、それに対する指示を行い、そこからのイベントを取得する。 イベントが発生するスレッドは任意のスレッドであるので、必ずGUIスレッドに変換してイベントの処理を行う。
ここでは長い処理の実行中にはProgressDialogを表示するが、Activityが停止あるいは破棄された場合、またビジネスロジックが処理終了した場合にこれをクローズするのは、GUI側だけの責任となる。具体的には、
ActivityのonPauseが呼び出されたら、ProgressDialogをクローズする。
ビジネスロジック側からの終了通知によってProgressDialogをクローズする。
前者の場合、長い処理がまだ進行中である可能性があるが、その場合はonResumeにて再度ProgressDialogを表示する。