Locked History Actions

Diff for "Android/View_not_attached_to_window_manager"

Differences between revisions 14 and 15
Deletions are marked like this. Additions are marked like this.
Line 163: Line 163:
ここが従来のGUI環境とは全く異なる。従来のGUI環境であれば、ウインドウが不意に消失してしまうことは基本的に想定する必要ないのである。 ここが従来のGUI環境とは全く異なる。従来のGUI環境であれば、ウインドウが不意に消失してしまうこと想定する必要などないのである。
Line 166: Line 166:
しかし、Androidアプリのビジネスロジックは、Activityとは無関係な流れを形成していなくてはならないのであり、Activityというものはその流れの中のある局面をユーザに表示するためだけの役割でなくてはいけない。 しかし、Androidアプリのビジネスロジックは、Activityとは無関係な流れを形成していなくてはならないのであり、Activityというものはその流れの中のある局面をユーザに表示するためだけの役割でなくてはいけない。あるいは、ビジネスロジック側がユーザ入力を受け付けたい場合にのみ使用される「従」の立場でなければならない。つまり、発想を完全に逆転しなければならないのだ。

アクティビティにはAndroidアプリの中に流れているビジネスロジックの息継ぎ場所の役割しか負わせてはならないのである。
Line 170: Line 172:
なぜなら、これではビジネスロジックとGUIがごちゃまぜになるし、アプリ中の他のビジネスロジックとの連携がとりにくくな なぜなら、これではビジネスロジックとGUIがごちゃまぜになるし、アプリ中の他のビジネスロジックとの連携がとりにくくなってしまう

View not attached to window manager

例外の発生状況

ProgressDialogAsyncTaskの中で安易に使用すると、

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がないせいなのか(このあたりの事情はもちろんよくわからない)、例外が発生してしまう。

この問題の議論状況

あまたに存在するウェブ上のリソース等は、この問題を放置したままである。「長時間処理を行う場合のサンプル」として公開されているものはどれもこれもこの問題を気にもとめていないものであって、端的に言えばバグっているのである。

まともに議論されているものとしては、例えば以下がある。

解決方法

解決方法として示されているものは以下の通り。

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の回転を拒否することはしたくないんだ。 やりたいことと言えばこうだ。

  1. プログレスダイアログは動的な情報を表示する。例えば、「サーバに接続中」とか「データをダウンロード中」とか。
  2. スレッドが重い処理を行い、ダイアログを更新する。
  3. 最後に、結果をUIに表示する。

問題は、スクリーンが回転すると、本に書かれているすべての解決策は失敗するということなんだ。 こういった状況で「正しい」やり方であるはずのAsyncTaskクラスを使っても。 スクリーンが回転すると、スレッドがやりとりしている「現在のコンテキスト」は消滅してしまう。そして、表示中のダイアログは失敗してしまう。 いかなるトリック(新しいコンテキストを実行中のスレッドに与えようと、スレッド状態を回転中に保持しようと)をコードに追加しようと、問題はいつでもダイアログなんだ。 コードは複雑になるが、必ず失敗してしまうんだ。

うまく行った唯一の解決策はActivity/Dialogトリックだ。

  1. ダイアログを作成して表示するのではなく、マニフェストでandroid:theme="@android:style/Theme.Dialog"と設定したActivityを作成する。これはダイアログのように見える。
  2. showDialog(DIALOG_ID)ではなく、startActivityForResult(yourActivityDialog, yourCode)にする。
  3. Use onActivityResult in the calling Activity to get the results from the executing thread (even the errors) and update the UI.
  4. 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アプリの中に流れているビジネスロジックの息継ぎ場所の役割しか負わせてはならないのである。

そして、このビジネスロジックの「流れ」をどこに保持すべきであるかと言えば、それはApplicationクラスの中以外には無い。 上記の解決策の中でActivity中にstaticとして保持するといったものがあるが、これは推奨できない。 なぜなら、これではビジネスロジックとGUIがごちゃまぜになるし、アプリ中の他のビジネスロジックとの連携がとりにくくなってしまう。

Androidアプリのあるべき構造

続く。。。