Hatena::Groupandroid

lnzntの Android 日記 このページをアンテナに追加 RSSフィード

2012年04月12日(木)

アクティビティ状態の保存/復元

22:57 | アクティビティ状態の保存/復元 - lnzntの Android 日記 を含むブックマーク はてなブックマーク - アクティビティ状態の保存/復元 - lnzntの Android 日記 アクティビティ状態の保存/復元 - lnzntの Android 日記 のブックマークコメント

アクティビティは onSaveInstanceState() と onRestoreInstanceState() で状態の保存/復元をできるようです。

onSaveInstanceState()

onSaveInstanceState() は onStop() の前に呼ばれます。

  • onPause() -> onSaveInstanceState() -> onStop()

ただし必ず呼ばれる訳ではなく、明確にアクティビティが破棄されるなど保存の必要がない場合は呼び出されません。

onRestoreInstanceState()

onRestoreInstanceState() は onResume() の前に呼ばれます。

  • onCreate() -> onStart() -> onRestoreInstanceState() -> onResume()

こちらも必ず呼ばれる訳でなく、以下などのように復元の必要がない場合は呼び出されません。

  • 最初の onCreate() -> onStart() -> onResume() の場合は呼ばれない
  • onRestart() -> onStart() -> onResume() の場合は呼ばれない

----

前回作った AlertDialog を状態の保存/復元するように変更してみました。

そして、もしダイアログが表示したまま画面回転した場合やホームに戻った場合などは、再起動した時にダイアログを再表示させるようにしてみました。

f:id:lnznt:20120412224414j:image

以下、コードです。

package com.example.alertdialogsample;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.util.Log;

public class AlertDialogSampleActivity extends Activity {
    private static final String logTag = "AlertDialogSample";
    private Activity activity = AlertDialogSampleActivity.this;
    
    private DialogInterface.OnClickListener clickListener;

    private AlertDialog alertDialog;
    
    private final static String shownKey   = ":shown";
    private final static String clickedKey = ":clicked";
    private final static String statusKey  = ":status";
    
    private boolean shown;
    private boolean clicked;
    private String status;
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        Log.d(logTag, "**** onCreate() ****");
        
        //
        // リスナの生成
        //
        clickListener = new DialogInterface.OnClickListener() {         
            @Override
            public void onClick(DialogInterface dialog, int which) {
                int labelId;
                
                switch (which) {
                case AlertDialog.BUTTON_POSITIVE:
                    labelId = R.string.label_positive;
                    break;
                case AlertDialog.BUTTON_NEGATIVE:
                    labelId = R.string.label_negative;
                    break;
                default:
                    labelId = R.string.label_neutral;
                    break;
                }
                        
                String label = activity.getString(labelId);
                Log.d(logTag, "[" + label + "] clicked.");
        
                shown   = false;
                clicked = true; 
                status  = new String(label);
            }
        };
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    
        Log.d(logTag, "**** onDestroy() ****");
    }
    
    @Override
    public void onRestart() {
        super.onRestart();
        
        Log.d(logTag, "**** onRestart() ****");
    }
    
    @Override
    public void onStart() {
        super.onStart();
        
        Log.d(logTag, "**** onStart() ****");
    }
    
    @Override
    public void onStop() {
        super.onStop();
        
        Log.d(logTag, "**** onStop() ****");
    } 
     
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        
        Log.d(logTag, "**** onSaveInstanceState() ****");
  
        outState.putBoolean(shownKey, shown);
        outState.putBoolean(clickedKey, clicked);       
        outState.putString(statusKey, status);
        
        Log.d(logTag, "saved: " + shownKey   + " <= " + shown); 
        Log.d(logTag, "saved: " + clickedKey + " <= " + clicked);       
        Log.d(logTag, "saved: " + statusKey  + " <= " + status);
    }
    
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        
        Log.d(logTag, "**** onRestoreInstanceState() ****");
  
        shown   = savedInstanceState.getBoolean(shownKey);
        clicked = savedInstanceState.getBoolean(clickedKey);
        status  = savedInstanceState.getString(statusKey);
 
        Log.d(logTag, "restore: " + shownKey   + " => " + shown);
        Log.d(logTag, "restore: " + clickedKey + " => " + clicked);     
        Log.d(logTag, "restore: " + statusKey  + " => " + status);
    }
    
    @Override
    public void onResume() {
        super.onResume();
        
        Log.d(logTag, "**** onResume() ****");
        
        Log.d(logTag, "shown:"   + shown);
        Log.d(logTag, "clicked:" + clicked);
        Log.d(logTag, "status:"  + status);
        
        //
        // ダイアログの生成
        //
        alertDialog = new AlertDialog.Builder(activity)
            .setTitle(R.string.dialog_title)
            .setMessage(R.string.dialog_message)
            .setPositiveButton(R.string.label_positive, clickListener)
            .setNegativeButton(R.string.label_negative, clickListener)
            .setNeutralButton(R.string.label_neutral, clickListener)
            .create();
        
        Log.d(logTag, "alert dialog created.");
        
        //
        // shown が TRUE だったらダイアログ表示
        //
        if (shown == true) {
            clicked = false;
            status  = "(none)";     
            
            alertDialog.show();
            
            Log.d(logTag, "alert dialog resumed.");
        }
    }
    
    @Override
    public void onPause() {
        super.onPause();
        
        Log.d(logTag, "**** onPause() ****");
        
        //
        // ダイアログの終了処理
        //
        alertDialog.dismiss();
        
        Log.d(logTag, "alert dialog dismissed.");
    }
     
    public void onClick(View v) {
        Log.d(logTag, "**** onClick() ****");
        
        //
        // ダイアログの表示
        //
        shown   = true;
        clicked = false;
        status  = "(none)";     
        
        alertDialog.show();
        
        Log.d(logTag, "alert dialog shown.");
    }
}

Alert Dialog

19:06 | Alert Dialog - lnzntの Android 日記 を含むブックマーク はてなブックマーク - Alert Dialog - lnzntの Android 日記 Alert Dialog - lnzntの Android 日記 のブックマークコメント

Alert Dialog を使うサンプルを書いてみました。

f:id:lnznt:20120412185609j:image

src/com.example.alertdialogsample/AlertDialogSampleActivity.java

package com.example.alertdialogsample;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.util.Log;

public class AlertDialogSampleActivity extends Activity {
    private static final String logTag = "AlertDialogSample";
    private Activity activity = AlertDialogSampleActivity.this;

    private DialogInterface.OnClickListener     positiveListener,
                                                negativeListener,
                                                neutralListener;
    private AlertDialog alertDialog;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.d(logTag, "**** onCreate() ****");

        //
        // リスナの生成
        //
        positiveListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String label = activity.getString(R.string.label_positive);
                Log.d(logTag, "[" + label + "] clicked.");
            }
        };

        negativeListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String label = activity.getString(R.string.label_negative);
                Log.d(logTag, "[" + label + "] clicked.");
            }
        };

        neutralListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String label = activity.getString(R.string.label_neutral);
                Log.d(logTag, "[" + label + "] clicked.");
            }
        };
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(logTag, "**** onDestroy() ****");
    }

    @Override
    public void onRestart() {
        super.onRestart();
        Log.d(logTag, "**** onRestart() ****");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(logTag, "**** onStart() ****");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d(logTag, "**** onStop() ****");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(logTag, "**** onResume() ****");

        //
        // ダイアログの生成
        //
        alertDialog = new AlertDialog.Builder(activity)
            .setTitle(R.string.dialog_title)
            .setMessage(R.string.dialog_message)
            .setPositiveButton(R.string.label_positive, positiveListener)
            .setNegativeButton(R.string.label_negative, negativeListener)
            .setNeutralButton(R.string.label_neutral, neutralListener)
            .create();

        Log.d(logTag, "alert dialog created.");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(logTag, "**** onPause() ****");

        //
        // ダイアログの終了処理
        //
        alertDialog.dismiss();

        Log.d(logTag, "alert dialog dismissed.");
    }

    public void onClick(View v) {
        Log.d(logTag, "**** onClick() ****");

        //
        // ダイアログの表示
        //
        alertDialog.show();
        Log.d(logTag, "alert dialog show.");
    }
}

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button1_label" android:onClick="onClick"/>

</LinearLayout>

res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">AlertDialogSample</string>
    <string name="button1_label">POP UP</string>
    <string name="dialog_title">Alert Dialog</string>
    <string name="dialog_message">ボタンを押してください</string>
    <string name="label_positive">OK</string>
    <string name="label_negative">NG</string>
    <string name="label_neutral">Cancel</string>
</resources>

余談

ググると「AlertDialog#show した状態で画面回転すると "leaked window"が起きる」という記事が多くみられました。

画面回転するとプロセスが kill されて*1、新しいプロセスが起動します。

原因は、その遷移の中で AlertDialog#dismiss を呼んでないことのようです。

上のコードではその事象は起きません。

「ダイアログの応答には、肯定/否定/中立 と "無応答という応答" がある」という仕様を前提にプログラムを設計/実装する必要がありそうです。

----

onStop() でダイアログ破棄がいい気がするのですが、Paused 状態からいきなり Process kill もありえるので止むを得ません。

Android Activity Lifecycle

さらに、余談

ちなみに、多くの記事で対処法としてあげられている Activity#showDialog (と Activity#onCreateDialog)は、既に deprecated になっているようです。

さらに、もうちょい余談

以下、"leaked window"が起きるコードです。

onCreate などにもログを入れてるので CatLog を見ていると何故リークするのか納得できます。

public class AlertDialogBadSampleActivity extends Activity {
    final static String logTag = "AlertDialogBadSample";

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.d(logTag, "onCreate() called.");
     }

    @Override
    public void onRestart() {
        super.onRestart();
        Log.d(logTag, "onRestart() called.");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(logTag, "onStart() called.");
    }
    
    @Override
    public void onResume() {
        super.onResume();       
        Log.d(logTag, "onResume() called.");
    }
    
    @Override
    public void onPause() {
        super.onPause();    
        Log.d(logTag, "onPause() called.");
    }
    
    @Override
    public void onStop() {
        super.onStop();     
        Log.d(logTag, "onStop() called.");
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();      
        Log.d(logTag, "onDestroy() called.");
    }  
    
    public void onClick(View v) {
        Log.d(logTag, "onClick() called.");
        
        AlertDialog alertDialog = new AlertDialog.Builder(AlertDialogSampleActivity.this)
                .setTitle(R.string.dialog_title)
                .setMessage(R.string.dialog_message)
                .setPositiveButton(R.string.label_positive, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Log.d(logTag, "[OK] clicked.");

                        dialog.dismiss();
                        Log.d(logTag, "dialog dismissed.");
                    }
                })
                .create();

        Log.d(logTag, "alert dialog created.");

        alertDialog.show();
    }
}

このコードでダイアログ表示中に画面を回転させると以下のエラーが出ます。

E/WindowManager(1394): Activity com.example.alertdialogsample.AlertDialogSampleActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@412e31c8 that was originally added here

*1Android Developers の開発ガイドを読んだ限りでは、内部的には、必ずしも kill されているとは限らないようです